Skip to content

Commit

Permalink
Migrate password hashing to BCrypt using Spring Security
Browse files Browse the repository at this point in the history
- Replaced SHA-1 hashing with BCryptPasswordEncoder for enhanced password security.
- Introduced PasswordHashHelper to centralize encoding logic, supporting both BCrypt and legacy SHA-1 hashes via DelegatingPasswordEncoder.
- Enhanced SecurityManager to handle password encoding, validation, and hash upgrades.
- Updated login and password management flows to utilize SecurityManager for secure handling.
- Refactored EncryptionUtils to delegate password hashing to PasswordHashHelper.
- Added spring-security-crypto to pom.xml for access to robust cryptography utilities.
Ensured compatibility with legacy hashes while enabling secure migration to BCrypt.
  • Loading branch information
chitrankd committed Dec 21, 2024
1 parent 2188422 commit 509e2c4
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 124 deletions.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,13 @@
<version>1.78.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-crypto -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>5.8.16</version>
</dependency>

<!-- AXIS2 (for OLIS) -->

<dependency>
Expand Down
23 changes: 3 additions & 20 deletions src/main/java/com/quatro/web/admin/SecurityAddSecurityHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,14 @@
*/
package com.quatro.web.admin;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

import javax.servlet.ServletRequest;
import javax.servlet.jsp.PageContext;

import org.oscarehr.common.dao.SecurityDao;
import org.oscarehr.common.model.Security;
import org.oscarehr.util.MiscUtils;
import org.oscarehr.managers.SecurityManager;
import org.oscarehr.util.SpringUtils;

import oscar.MyDateFormat;
Expand All @@ -46,6 +44,7 @@
public class SecurityAddSecurityHelper {

private SecurityDao securityDao = SpringUtils.getBean(SecurityDao.class);
private final SecurityManager securityManager = SpringUtils.getBean(SecurityManager.class);

/**
* Adds a security record (i.e. user login information) for the provider.
Expand All @@ -60,26 +59,10 @@ public void addProvider(PageContext pageContext) {
pageContext.setAttribute("message", message);
}

public static String digestPassword(String rawPassword) throws NoSuchAlgorithmException {
StringBuilder sbTemp = new StringBuilder();
MessageDigest md = MessageDigest.getInstance("SHA");
byte[] btNewPasswd = md.digest(rawPassword.getBytes());
for (int i = 0; i < btNewPasswd.length; i++)
sbTemp = sbTemp.append(btNewPasswd[i]);
return sbTemp.toString();
}

private String process(PageContext pageContext) {
ServletRequest request = pageContext.getRequest();

String digestedPassword = null;

try {
digestedPassword = digestPassword(request.getParameter("password"));
} catch (NoSuchAlgorithmException e) {
MiscUtils.getLogger().error("Unable to get SHA message digest", e);
return "admin.securityaddsecurity.msgAdditionFailure";
}
String digestedPassword = this.securityManager.encodePassword(request.getParameter("password"));

boolean isUserRecordAlreadyCreatedForProvider = !securityDao.findByProviderNo(request.getParameter("provider_no")).isEmpty();
if (isUserRecordAlreadyCreatedForProvider) return "admin.securityaddsecurity.msgLoginAlreadyExistsForProvider";
Expand Down
100 changes: 65 additions & 35 deletions src/main/java/org/oscarehr/managers/SecurityManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,26 @@
*/
package org.oscarehr.managers;

import java.security.MessageDigest;
import java.util.Date;
import java.util.List;

import org.apache.cxf.common.util.StringUtils;
import org.apache.logging.log4j.Logger;
import org.oscarehr.common.dao.SecurityArchiveDao;
import org.oscarehr.common.dao.SecurityDao;
import org.oscarehr.common.model.Security;
import org.oscarehr.util.EncryptionUtils;
import org.oscarehr.util.LoggedInInfo;
import org.oscarehr.util.MiscUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import oscar.OscarProperties;
import oscar.log.LogAction;

import java.util.Date;
import java.util.List;

@Service
public class SecurityManager {

//private static Logger logger = MiscUtils.getLogger();
private static final Logger logger = MiscUtils.getLogger();

@Autowired
private SecurityDao securityDao;
Expand Down Expand Up @@ -85,16 +85,16 @@ public boolean checkPasswordAgainstPrevious(String newPassword, String providerN
String previousPasswordPolicy = OscarProperties.getInstance().getProperty("password.pastPasswordsToNotUse", "0");
try {
Security dbSecurity = securityDao.getByProviderNo(providerNo);
if(!"0".equals(previousPasswordPolicy) && !validatePassword(newPassword, dbSecurity.getPassword())) {

if (!"0".equals(previousPasswordPolicy) && !this.matchesPassword(newPassword, dbSecurity.getPassword())) {

int numToGoBack = Integer.parseInt(previousPasswordPolicy);
List<String> archives = securityArchiveDao.findPreviousPasswordsByProviderNo(providerNo,numToGoBack);

boolean foundItInPast=false;

for(String a:archives) {
if(validatePassword(newPassword, a)) {
if (this.matchesPassword(newPassword, a)) {
foundItInPast = true;
break;
}
Expand All @@ -110,34 +110,64 @@ public boolean checkPasswordAgainstPrevious(String newPassword, String providerN
}
return false;
}

private boolean validatePassword(String newPassword, String existingPassword) {

try {
String p1 = encodePassword(newPassword);
if(p1.equals(existingPassword)) {
return true;
}
} catch(Exception e) {
MiscUtils.getLogger().error("Error",e);

/**
* Encode the given password using the configured hashing algorithm.
*
* @param password The password to encrypt.
* @return The encrypted password.
*/
public String encodePassword(CharSequence password) {
return EncryptionUtils.hash(password);
}

/**
* Validates the password against the provided security's stored password. If the password is valid and an upgrade
* is needed to the existing stored password, the stored password will be upgraded.
*
* @param rawPassword The password to validate.
* @param security The security object containing the stored password.
*/
public boolean validatePassword(CharSequence rawPassword, Security security) {
boolean isValid = this.matchesPassword(rawPassword, security.getPassword());
if (isValid && EncryptionUtils.isPasswordHashUpgradeNeeded(security.getPassword())) {
boolean isHashUpgraded = this.upgradeSavePasswordHash(rawPassword, security);
if (isHashUpgraded)
logger.error("Error while upgrading password hash");
}

return false;
return isValid;
}

/**
* Validates the password against the provided encoded password.
*
* @param rawPassword The password to validate.
* @param encodedPassword The encoded password to compare against.
* @return True if the password is valid, false otherwise.
*/
public boolean matchesPassword(CharSequence rawPassword, String encodedPassword) {
return EncryptionUtils.verify(rawPassword, encodedPassword);
}

/**
* Upgrades the password hash and saves the updated Security object.
*
* @param rawPassword The raw password to hash.
* @param security The Security object to update.
* @return True if the password hash was successfully upgraded and saved, false otherwise.
*/
public boolean upgradeSavePasswordHash(CharSequence rawPassword, Security security) {
String hash = this.encodePassword(rawPassword);
boolean matched = this.matchesPassword(rawPassword, hash);

if (!matched) // should never happen, but if password upgrade fails.
return false;

security.setPassword(hash);
this.securityDao.merge(security);
return true;
}

private String encodePassword(String password) throws Exception{

MessageDigest md = MessageDigest.getInstance("SHA");

StringBuilder sbTemp = new StringBuilder();
byte[] btNewPasswd= md.digest(password.getBytes());
for(int i=0; i<btNewPasswd.length; i++) sbTemp = sbTemp.append(btNewPasswd[i]);

return sbTemp.toString();

}


public Security findByProviderNo(LoggedInInfo loggedInInfo, String providerNo) {

List<Security> results = securityDao.findByProviderNo(providerNo);
Expand Down Expand Up @@ -250,4 +280,4 @@ protected boolean isSecurityObjectValid(Security security) {

return true;
}
}
}
33 changes: 24 additions & 9 deletions src/main/java/org/oscarehr/util/EncryptionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
package org.oscarehr.util;

import org.apache.logging.log4j.Logger;
import org.oscarehr.util.password.PasswordHashHelper;

import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
Expand All @@ -35,7 +36,7 @@
import java.util.Objects;


public final class EncryptionUtils extends PasswordHash {
public final class EncryptionUtils {
private static final QueueCacheValueCloner<byte[]> byteArrayCloner = new QueueCacheValueCloner<byte[]>() {
public byte[] cloneBean(byte[] original) {
return (byte[])original.clone();
Expand Down Expand Up @@ -283,25 +284,39 @@ public static void prepareSecretKeySpec() {
}

/**
* A one way PBKDF2 With Hmac SHA1 hash of given password string.
* The verify method, should be used to validate the hash against
* passwords.
* Generates a secure hash of the given password using the PasswordHashHelper.
*
* @see PasswordHashHelper#encodePassword(CharSequence)
* @param password string
* @return hashed password
* @throws CannotPerformOperationException failed encrypt
* @throws IllegalArgumentException If the password is null or empty.
*/
public static String hash(String password) throws CannotPerformOperationException {
return createHash(password);
public static String hash(CharSequence password) throws IllegalArgumentException {
return PasswordHashHelper.encodePassword(password);
}

/**
* Validate a given password phrase against the stored hash password.
* This is a boolean validation. No password values are returned
* @param password plain password string
* @see PasswordHashHelper#matches(CharSequence, String)
* @param hashedPassword hashed password string usually stored in the database.
* @return True if the raw password matches the encoded password, false otherwise.
* @throws IllegalArgumentException failure while matching
*/
public static boolean verify(CharSequence password, String hashedPassword) throws IllegalArgumentException {
return PasswordHashHelper.matches(password, hashedPassword);
}

/**
* Check if a given hashed password needs to be upgraded to a more secure
* algorithm.
* @param hashedPassword hashed password string usually stored in the database.
* @see PasswordHashHelper#upgradeEncoding(String)
* @return true if upgrade is needed.
*/
public static boolean verify(String password, String hashedPassword) throws InvalidHashException, CannotPerformOperationException {
return verifyPassword(password, hashedPassword);
public static boolean isPasswordHashUpgradeNeeded(String hashedPassword) {
return PasswordHashHelper.upgradeEncoding(hashedPassword);
}

static {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2005-2012. Centre for Research on Inner City Health, St. Michael's Hospital, Toronto. All Rights Reserved.
* This software is published under the GPL GNU General Public License.
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
* <p>
* This software was written for
* Centre for Research on Inner City Health, St. Michael's Hospital,
* Toronto, Ontario, Canada
*/

package org.oscarehr.util.password;


import org.oscarehr.util.MiscUtils;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.security.MessageDigest;


/**
* This class uses insecure SHA hashing for backwards compatibility while migrating to a newer hashing method.
* It will be removed in the future.
*/
public class Deprecated_SHA_PasswordEncoder implements PasswordEncoder {

@Override
public String encode(CharSequence rawPassword) {
try {
return this.encodeShaPassword(rawPassword.toString());
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return this.validateShaPassword(rawPassword.toString(), encodedPassword);
}

private boolean validateShaPassword(String newPassword, String existingPassword) {

try {
String p1 = this.encodeShaPassword(newPassword);
if (p1.equals(existingPassword)) {
return true;
}
} catch (Exception e) {
MiscUtils.getLogger().error("Error", e);
}

return false;
}

private String encodeShaPassword(String password) throws Exception {

MessageDigest md = MessageDigest.getInstance("SHA");

StringBuilder sbTemp = new StringBuilder();
byte[] btNewPasswd = md.digest(password.getBytes());
for (int i = 0; i < btNewPasswd.length; i++) sbTemp = sbTemp.append(btNewPasswd[i]);

return sbTemp.toString();

}
}
Loading

0 comments on commit 509e2c4

Please sign in to comment.