Skip to content

Commit

Permalink
Merge pull request #249 from ncats/pw_security
Browse files Browse the repository at this point in the history
WIP: Update UserProf to use new hash and salt
  • Loading branch information
blueSwordfish authored Mar 29, 2024
2 parents 6e73ba0 + bdffc49 commit 40f7956
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package gsrs.model;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
public class UserProfileAuthenticationResult {
private boolean matchesRepository;

public boolean matchesRepository() {
return matchesRepository;
}

public void setMatchesRepository(boolean matchesRepository) {
this.matchesRepository = matchesRepository;
}

public boolean needsSave() {
return needsSave;
}

public void setNeedsSave(boolean needsSave) {
this.needsSave = needsSave;
}

private boolean needsSave;
}
45 changes: 36 additions & 9 deletions gsrs-core-entities/src/main/java/ix/core/models/UserProfile.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package ix.core.models;


import ch.qos.logback.core.rolling.helper.TokenConverter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;

import gov.nih.ncats.common.util.CachedSupplier;
import gov.nih.ncats.common.util.TimeUtil;
import gsrs.model.UserProfileAuthenticationResult;
import gsrs.security.TokenConfiguration;
import gsrs.springUtils.StaticContextAccessor;
import gsrs.util.GsrsPasswordHasher;
import gsrs.util.Hasher;
import gsrs.util.LegacyTypeSalter;
import gsrs.util.Salter;
import ix.utils.Util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import javax.persistence.*;
import java.util.*;
Expand All @@ -22,7 +25,14 @@
@SequenceGenerator(name = "LONG_SEQ_ID", sequenceName = "ix_core_userprof_seq", allocationSize = 1)
@EntityListeners(UserProfileEntityProcessor.class)
public class UserProfile extends IxModel{
private static ObjectMapper om = new ObjectMapper();
private final static String SALT_PREFIX = "G";

private static ObjectMapper om = new ObjectMapper();

//todo: look into autowiring the salter and hasher
private static Salter salter = new LegacyTypeSalter(new GsrsPasswordHasher(), SALT_PREFIX);

private static Hasher hasher = new GsrsPasswordHasher();

private static CachedSupplier<UserProfile> GUEST_PROF= CachedSupplier.of(()->{
UserProfile up = new UserProfile(new Principal("GUEST"));
Expand Down Expand Up @@ -182,21 +192,38 @@ public boolean acceptToken(String token) {
return false;
}

public boolean acceptPassword(String password) {
if (this.hashp == null || this.salt == null)
return false;
return this.hashp.equals(Util.encrypt(password, this.salt));
public UserProfileAuthenticationResult acceptPassword(String password) {
UserProfileAuthenticationResult result = new UserProfileAuthenticationResult(false, false);
if (this.hashp == null || this.salt == null) {
return result;
}
boolean pwOk = this.hashp.equals(hasher.hash(password, this.salt));
log.trace("pwOk: {}", pwOk);
boolean legacyPwOk = false;
//when authentication using latest algorithms fails, see if the password works with the legacy methods
if( !pwOk) {
legacyPwOk= this.hashp.equals(Util.encrypt(password, this.salt));
}
if( legacyPwOk && !salter.mayBeOneOfMine(this.salt)) {
//we have a pw assigned using the older algorithm so we need to resalt and rehash
log.trace("going to request rehash of password");
setPassword(password);
result.setNeedsSave(true);
}
result.setMatchesRepository(pwOk || legacyPwOk);
return result;
}

public void setPassword(String password) {
if (password == null || password.length() <= 0) {
password = UUID.randomUUID().toString();
}
this.salt = Util.generateSalt();
this.hashp = Util.encrypt(password, this.salt);
this.salt = salter.generateSalt();
this.hashp = hasher.hash(password, salt); //Util.encrypt(password, this.salt);
setIsDirty("salt");
setIsDirty("hashp");
}

@Indexable(indexed = false)
@JsonIgnore
public String getEncodePassword(){
Expand Down
38 changes: 38 additions & 0 deletions gsrs-core-entities/src/test/java/gsrs/LegacySalterTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gsrs;

import gsrs.util.GsrsPasswordHasher;
import gsrs.util.Hasher;
import gsrs.util.LegacyTypeSalter;
import gsrs.util.Salter;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class LegacySalterTests {

private final String SALT_PREFIX = "G";

private final Hasher hasher = new GsrsPasswordHasher();

private final Salter salter = new LegacyTypeSalter(hasher, SALT_PREFIX);

@Test
void testGenerateSalt() {
String salt1 = salter.generateSalt();
Assertions.assertNotNull(salt1);
System.out.printf("salt: %s\n", salt1);
}

@Test
void testGenerateOwnSalt() {
String salt1 = salter.generateSalt();
Assertions.assertTrue(salter.mayBeOneOfMine(salt1));
}

@Test
void testGenerateNotOwnSalt() {
String salt1 = salter.generateSalt();
String changedSalt = salt1.replace('G', 'N');
Assertions.assertFalse(salter.mayBeOneOfMine(changedSalt));
}

}
62 changes: 62 additions & 0 deletions gsrs-core/src/main/java/gsrs/util/GsrsPasswordHasher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package gsrs.util;

import lombok.extern.slf4j.Slf4j;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

@Slf4j
public class GsrsPasswordHasher implements Hasher {

String preferredHashAlgorithm = "PBKDF2";
static int iterations = 1000;
static String characterSet ="utf8";

private final static String HASHING_ALGORITHM = "PBKDF2WithHmacSHA512";

@Override
public String getHashType() {
return this.preferredHashAlgorithm;
}

@Override
public String hash(String... values) {
if (values == null) {
return null;
}
try {
if(preferredHashAlgorithm.equals("PBKDF2")) {
return hash(values[0], values.length > 1 ? values[1] : null, iterations);
}
MessageDigest md = MessageDigest.getInstance(preferredHashAlgorithm);
for (String v : values) {
md.update(v.getBytes(characterSet));
}
return toHex(md.digest());
} catch (Exception ex) {
log.error("Can't generate hash!", ex);
throw new RuntimeException(ex);
}
}

public static String toHex(byte[] d) {
StringBuilder sb = new StringBuilder();
for (byte b : d) {
sb.append(String.format("%1$02x", b & 0xff));
}
return sb.toString();
}

public static String hash(String input, String salt, int iterations) throws NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException {
PBEKeySpec spec = new PBEKeySpec(input.toCharArray(), salt != null ? salt.getBytes(characterSet) :
input.getBytes(characterSet), iterations, 64 * 8);
SecretKeyFactory skf = SecretKeyFactory.getInstance(HASHING_ALGORITHM);

byte[] hash = skf.generateSecret(spec).getEncoded();
return toHex(hash);
}
}
8 changes: 8 additions & 0 deletions gsrs-core/src/main/java/gsrs/util/Hasher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package gsrs.util;

public interface Hasher {

String getHashType();

String hash(String... values);
}
34 changes: 34 additions & 0 deletions gsrs-core/src/main/java/gsrs/util/LegacyTypeSalter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package gsrs.util;

import gov.nih.ncats.common.util.TimeUtil;
import lombok.Data;

import static java.lang.String.valueOf;

@Data
public class LegacyTypeSalter implements Salter {

Hasher hasher;

String prefix = "";

public LegacyTypeSalter(Hasher newHasher, String newPrefix) {
hasher = newHasher;
prefix = newPrefix;
}
@Override
public void setHasher(Hasher hasher) {
this.hasher = hasher;
}

@Override
public String generateSalt() {
String text = "---" + TimeUtil.getCurrentDate().toString() + "---" + String.valueOf(Math.random()) + "---";
return prefix + hasher.hash(text);
}

@Override
public boolean mayBeOneOfMine(String testHash) {
return testHash != null && testHash.startsWith(prefix);
}
}
10 changes: 10 additions & 0 deletions gsrs-core/src/main/java/gsrs/util/Salter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gsrs.util;

public interface Salter {

void setHasher(Hasher hasher);

String generateSalt();

boolean mayBeOneOfMine(String testHash);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import gsrs.cache.GsrsCache;
import gsrs.controller.hateoas.GsrsUnwrappedEntityModel;
import gsrs.model.UserProfileAuthenticationResult;
import gsrs.repository.GroupRepository;
import gsrs.repository.SessionRepository;
import gsrs.repository.UserProfileRepository;
Expand All @@ -10,7 +11,6 @@
import ix.core.models.Session;
import ix.core.models.UserProfile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
Expand Down Expand Up @@ -135,7 +135,8 @@ public ResponseEntity<Object> changePassword(Principal principal,
return gsrsControllerConfiguration.handleBadRequest(400,"password can not be blank or all whitespace", queryParameters);
}
//check old password
if(!up.acceptPassword(passwordChangeRequest.getOldPassword())){
UserProfileAuthenticationResult authenticationResult =up.acceptPassword(passwordChangeRequest.getOldPassword());
if(!authenticationResult.matchesRepository()){
return gsrsControllerConfiguration.unauthorized("incorrect password", queryParameters);
}
up.setPassword(passwordChangeRequest.getNewPassword());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.Transactional;

import gsrs.model.UserProfileAuthenticationResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
Expand Down Expand Up @@ -172,9 +174,22 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

}
if(up!=null && up.active){
if(up.acceptPassword(pass)){
UserProfileAuthenticationResult authenticationResult =up.acceptPassword(pass);
if(authenticationResult.matchesRepository()){
//valid password!
auth = new UserProfilePasswordAuthentication(up);
if(authenticationResult.needsSave()){
UserProfile finalUp = up;
TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager);
UserProfile savedUp=transactionTemplate.execute(status -> {
Optional<UserProfile> opt = Optional.ofNullable(repository.findByUser_UsernameIgnoreCase(finalUp.user.username));
opt.get().setPassword(pass);
saveUserProfile(opt.get());
return opt.get();
});
auth = new UserProfilePasswordAuthentication(savedUp);
} else {
auth = new UserProfilePasswordAuthentication(up);
}

}else{
throw new BadCredentialsException("invalid credentials for username: " + username);
Expand Down Expand Up @@ -263,4 +278,10 @@ private UserProfile autoregisterNewUser(String username, String email, List<Role
repository.saveAndFlush(up);
return up;
}

@Transactional
private void saveUserProfile(UserProfile profile) {
log.trace("saving up within transaction");
repository.saveAndFlush(profile);
}
}
Loading

0 comments on commit 40f7956

Please sign in to comment.