Passwords, Hashing and Salting
Passwords have always been crucial for securing our data and online accounts. They are often the only line of defense against unauthorized access. Software engineers strive to ensure the safety and integrity of their users and systems. Nowadays, besides passwords, there are other security measures like SSO (Single Sign-On) and 2FA (Two-Factor Authentication). However, due to security concerns, some institutions such as banks, hospitals, governments, and schools cannot always rely on these options and must develop their secure solutions for password management and protection.
Hashing is a method of generating a unique “fingerprint” of data using an algorithm. It creates a fixed-size string for any input and is a secure way to store sensitive information. It’s important to note that the original data cannot be derived from the hash. It’s like trying to identify a person based on a fingerprint alone - it’s impossible. However, given a large database of fingerprints, finding a matching one would be theoretically feasible.
Bcrypt
BCrpyt is a password-hashing function known for its secure approach and iterative work factor capabilities. Developers are required to have password hashes stored in the database instead of real plaintext passwords. There are several hashing algorithms such as SHA-256 and MD5. These hashes however are vulnerable to rainbow table attacks. Rainbow table attack can be summarised as pre-computing a bulk amount of password hashes. Since hash functions provide the same output for the same input, pre-calculated password hashes can be used to brute force known password hashes.
Salting is an approach to counter these rainbow table attack vulnerabilities. You simply concatenate a text to the password before getting the hash of it. While this is a good approach to solving this issue, a known salt can make things no different than no salt. Applying the same salt to each password creates another vulnerability. So, it is required to have a unique salt for each password. BCrpyt generates a unique salt for each password and creates the hash of the password + salt combination. This way, all password hashes have different salts which make them protected against brute-force attacks.
Passay and Cryptacular
Passay is a password-validation library created. It is a great tool to be used alongside the spring validations. I wrote a blogpost blog post back in 2022 explaining the usages of passay in a brief detail.
Cryptacular on the other hand is a cryptography library consisting of hashing, Encryption & Decryption, key management, etc. We will use cryptacular alongside the passay to implement old hashed password rule control.
Practial Example
Source code used for practical examples can be found on my GitHub repository
Tech Stack:
-
Spring Boot Framework 3.3.3 (inc. JPA, Validation, Web and Security)
-
H2 Database (In memory database)
Let’s start by creating a user entity and DTO containing a username and password. Next, implement a repository and services to register the user to the database. Next, we need to handle the user password change. When validating the user password, we need to check the old user passwords and make sure that the new password does not match the number of old passwords (let’s say the last 3 passwords). Here the PasswordChangeValidator checks the old hashed user passwords and returns false if the new password is the same as one of the old three passwords.
@Component
public class PasswordChangeValidator implements ConstraintValidator<ValidPasswordChange, PasswordChangeRequest> {
private final BCryptHashBean bCryptHashBean;
private final UserPasswordsService userPasswordsService;
public PasswordChangeValidator(BCryptHashBean bCryptHashBean, UserPasswordsService userPasswordsService) {
this.bCryptHashBean = bCryptHashBean;
this.userPasswordsService = userPasswordsService;
}
@Override
public void initialize(ValidPasswordChange constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(PasswordChangeRequest passwordChangeRequest, ConstraintValidatorContext constraintValidatorContext) {
//Initializing the PasswordValidator with the DigestHistoryRule
PasswordValidator passwordValidator = new PasswordValidator(
List.of(
new DigestHistoryRule(bCryptHashBean)
)
);
//Getting the old passwords of the user from the database
final List<PasswordData.Reference> oldPasswords = userPasswordsService
.getUserPasswords(passwordChangeRequest.getId()).stream()
.map(s -> (PasswordData.Reference) new PasswordData.HistoricalReference(s)).toList();
//Creating a PasswordData object with the new password and the old passwords
final PasswordData passwordData = new PasswordData(passwordChangeRequest.getUsername(),
passwordChangeRequest.getNewPassword(), oldPasswords);
//Validating the password. Checking if it does match the DigestHistoryRule
RuleResult result = passwordValidator.validate(passwordData);
if (result.isValid()) {
return true;
}
//If the password does not match the DigestHistoryRule, adding the error messages to the context
constraintValidatorContext.buildConstraintViolationWithTemplate(String.join("", passwordValidator.getMessages(result)))
.addConstraintViolation()
.disableDefaultConstraintViolation();
return false;
}
}
We are also required to create a class validation annotation (Type annotation) to validate the password change request (Since we need the userID to get the old user passwords).
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = PasswordChangeValidator.class)
public @interface ValidPasswordChange {
String message() default "Invalid password!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Next, simply create a password change request dto. Note that we used @ValidPasswordChange annotation on a class level instead of a field level.
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ValidPasswordChange
public class PasswordChangeRequest {
private Integer id;
private String username;
private String newPassword;
}
On the service side, we need a method to get old user passwords from the database. Notice that from the repository it’s getting the newest top three passwords. This means we are controlling the new input password against the old latest three passwords.
@Override
public List<String> getUserPasswords(Integer userId) {
final List<UserPasswords> userPasswordsList = userPasswordsRepository.findTop3ByUserIdOrderByCreatedDateDesc(userId);
final List<UserPasswordsDto> userPasswordsDtoList = userPasswordsMapper.toUserPasswordsDto(userPasswordsList);
if(userPasswordsDtoList.isEmpty()){
throw new RuntimeException("User has no password history");
}
return userPasswordsDtoList.stream().map(UserPasswordsDto::getPassword).toList();
}
Lastly, we need to implement an ExceptionHandler for MethodArgumentNotValidException (thrown by Passay if the password is not valid, or by Spring Validation)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<BaseResponse> handle(MethodArgumentNotValidException e) {
final String validationErrorMessages = Arrays.stream(Objects.requireNonNull(e.getDetailMessageArguments()))
.map(Objects::toString)
.collect(Collectors.joining());
return ResponseEntity.badRequest()
.body(BaseResponse.builder()
.timestamp(Instant.now())
.message(validationErrorMessages)
.build());
}
Demo and Results
Let’s cement the blog with a demo. Start by registering a user to a database
Note that this request created a new record on both user and user_password tables.
Now, let’s change the user password 2 times.
Hashes of the old passwords are stored on the user_passwords table. The program will get the latest three of these records by user_id, and check if the supplied new password matches the old password hashes.
Now let’s try to supply an old password as a new password. The program notices it matches with one of the old passwords and throws an exception.
Conclusion
This blog post was about showing a practical use case for Passay’s DigestHistoryRule. DigestHistoryRule is a password policy rule that forbids users to use old passwords from being used again. However since the passwords are stored in highly specialized and salted hash forms, a special approach is needed to control these new passwords. DigestHistoryRule can be applied alongside other password rules. Please, do not hesitate to contact me if you have any questions, doubts, or fixing requirements about this blog post. You can contact me on my website.