Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ public ResponseEntity<JSONResponse> registerUserAccount(@Valid @RequestBody User
log.error("Unexpected error during registration.", ex);
logAuditEvent("Registration", "Failure", ex.getMessage(), null, request);
return buildErrorResponse("System Error!", 5, HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
// Clear sensitive password data from memory
if (userDto != null) {
userDto.clearPasswords();
}
}
}

Expand Down Expand Up @@ -244,6 +249,11 @@ public ResponseEntity<JSONResponse> savePassword(@Valid @RequestBody SavePasswor
log.error("Unexpected error during password reset.", ex);
logAuditEvent("PasswordReset", "Failure", ex.getMessage(), null, request);
return buildErrorResponse("System Error!", 5, HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
// Clear sensitive password data from memory
if (savePasswordDto != null) {
savePasswordDto.clearPasswords();
}
}
}

Expand Down Expand Up @@ -293,6 +303,11 @@ public ResponseEntity<JSONResponse> updatePassword(@AuthenticationPrincipal DSUs
log.error("Unexpected error during password update.", ex);
logAuditEvent("PasswordUpdate", "Failure", ex.getMessage(), user, request);
return buildErrorResponse("System Error!", 5, HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
// Clear sensitive password data from memory
if (passwordDto != null) {
passwordDto.clearPasswords();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.digitalsanctuary.spring.user.dto;

import com.digitalsanctuary.spring.user.util.PasswordSecurityUtil;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
Expand All @@ -8,9 +9,13 @@
/**
* A new password dto. This object is used for password form actions (change password, forgot password token, save
* password, etc..).
*
* <p>Note: This DTO supports both String and char[] for passwords. The char[] methods are preferred
* for enhanced security as they allow explicit memory clearing. String methods are maintained
* for backward compatibility.
*/
@Data
public class PasswordDto {
public class PasswordDto implements AutoCloseable {

/** The old password. */
@ToString.Exclude
Expand All @@ -26,4 +31,86 @@ public class PasswordDto {
/** The token. */
private String token;

/** The old password as char array (for secure handling). */
@ToString.Exclude
private transient char[] oldPasswordChars;

/** The new password as char array (for secure handling). */
@ToString.Exclude
private transient char[] newPasswordChars;

/**
* Gets the old password as a char array for secure handling.
* If oldPasswordChars is null but oldPassword is set, converts from String.
*
* @return the old password as char array, or null if not set
*/
public char[] getOldPasswordChars() {
if (oldPasswordChars == null && oldPassword != null) {
return PasswordSecurityUtil.toCharArray(oldPassword);
}
return oldPasswordChars;
}

/**
* Sets the old password from a char array (preferred for security).
* Also updates the String oldPassword field for backward compatibility.
*
* @param oldPasswordChars the old password as char array
*/
public void setOldPasswordChars(char[] oldPasswordChars) {
this.oldPasswordChars = oldPasswordChars;
if (oldPasswordChars != null) {
this.oldPassword = PasswordSecurityUtil.toString(oldPasswordChars);
} else {
this.oldPassword = null;
}
}

/**
* Gets the new password as a char array for secure handling.
* If newPasswordChars is null but newPassword is set, converts from String.
*
* @return the new password as char array, or null if not set
*/
public char[] getNewPasswordChars() {
if (newPasswordChars == null && newPassword != null) {
return PasswordSecurityUtil.toCharArray(newPassword);
}
return newPasswordChars;
}

/**
* Sets the new password from a char array (preferred for security).
* Also updates the String newPassword field for backward compatibility.
*
* @param newPasswordChars the new password as char array
*/
public void setNewPasswordChars(char[] newPasswordChars) {
this.newPasswordChars = newPasswordChars;
if (newPasswordChars != null) {
this.newPassword = PasswordSecurityUtil.toString(newPasswordChars);
} else {
this.newPassword = null;
}
}

/**
* Clears sensitive password data from memory.
* This method should be called in a finally block to ensure passwords are cleared.
*/
public void clearPasswords() {
PasswordSecurityUtil.clearPassword(oldPasswordChars);
PasswordSecurityUtil.clearPassword(newPasswordChars);
oldPasswordChars = null;
newPasswordChars = null;
}

/**
* Closes this resource, clearing password data from memory.
*/
@Override
public void close() {
clearPasswords();
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,120 @@
package com.digitalsanctuary.spring.user.dto;

import com.digitalsanctuary.spring.user.util.PasswordSecurityUtil;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.ToString;

/**
* Data Transfer Object for saving a new password after password reset.
* Used in the password reset flow after the user clicks the link in their email
* and enters a new password.
*
* <p>Note: This DTO supports both String and char[] for passwords. The char[] methods are preferred
* for enhanced security as they allow explicit memory clearing. String methods are maintained
* for backward compatibility.
*/
@Data
public class SavePasswordDto {
public class SavePasswordDto implements AutoCloseable {

/** The password reset token from the email link. */
@NotNull
@NotEmpty
private String token;

/** The new password to set. */
@ToString.Exclude
@NotNull
@NotEmpty
private String newPassword;

/** Confirmation of the new password (must match newPassword). */
@ToString.Exclude
@NotNull
@NotEmpty
private String confirmPassword;

/** The new password as char array (for secure handling). */
@ToString.Exclude
private transient char[] newPasswordChars;

/** The confirm password as char array (for secure handling). */
@ToString.Exclude
private transient char[] confirmPasswordChars;

/**
* Gets the new password as a char array for secure handling.
* If newPasswordChars is null but newPassword is set, converts from String.
*
* @return the new password as char array, or null if not set
*/
public char[] getNewPasswordChars() {
if (newPasswordChars == null && newPassword != null) {
return PasswordSecurityUtil.toCharArray(newPassword);
}
return newPasswordChars;
}

/**
* Sets the new password from a char array (preferred for security).
* Also updates the String newPassword field for backward compatibility.
*
* @param newPasswordChars the new password as char array
*/
public void setNewPasswordChars(char[] newPasswordChars) {
this.newPasswordChars = newPasswordChars;
if (newPasswordChars != null) {
this.newPassword = PasswordSecurityUtil.toString(newPasswordChars);
} else {
this.newPassword = null;
}
}

/**
* Gets the confirm password as a char array for secure handling.
* If confirmPasswordChars is null but confirmPassword is set, converts from String.
*
* @return the confirm password as char array, or null if not set
*/
public char[] getConfirmPasswordChars() {
if (confirmPasswordChars == null && confirmPassword != null) {
return PasswordSecurityUtil.toCharArray(confirmPassword);
}
return confirmPasswordChars;
}

/**
* Sets the confirm password from a char array (preferred for security).
* Also updates the String confirmPassword field for backward compatibility.
*
* @param confirmPasswordChars the confirm password as char array
*/
public void setConfirmPasswordChars(char[] confirmPasswordChars) {
this.confirmPasswordChars = confirmPasswordChars;
if (confirmPasswordChars != null) {
this.confirmPassword = PasswordSecurityUtil.toString(confirmPasswordChars);
} else {
this.confirmPassword = null;
}
}

/**
* Clears sensitive password data from memory.
* This method should be called in a finally block to ensure passwords are cleared.
*/
public void clearPasswords() {
PasswordSecurityUtil.clearPassword(newPasswordChars);
PasswordSecurityUtil.clearPassword(confirmPasswordChars);
newPasswordChars = null;
confirmPasswordChars = null;
}

/**
* Closes this resource, clearing password data from memory.
*/
@Override
public void close() {
clearPasswords();
}
}
90 changes: 89 additions & 1 deletion src/main/java/com/digitalsanctuary/spring/user/dto/UserDto.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.digitalsanctuary.spring.user.dto;

import com.digitalsanctuary.spring.user.util.PasswordSecurityUtil;
import com.digitalsanctuary.spring.user.validation.PasswordMatches;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
Expand All @@ -10,10 +11,14 @@
/**
* A user dto. This object is used for handling user related form data (registration, forms passing in email addresses,
* etc...).
*
* <p>Note: This DTO supports both String and char[] for passwords. The char[] methods are preferred
* for enhanced security as they allow explicit memory clearing. String methods are maintained
* for backward compatibility.
*/
@Data
@PasswordMatches
public class UserDto {
public class UserDto implements AutoCloseable {

/** The first name. */
@NotBlank(message = "First name is required")
Expand Down Expand Up @@ -44,4 +49,87 @@ public class UserDto {

/** The role. */
private Integer role;

/** The password as char array (for secure handling). */
@ToString.Exclude
private transient char[] passwordChars;

/** The matching password as char array (for secure handling). */
@ToString.Exclude
private transient char[] matchingPasswordChars;

/**
* Gets the password as a char array for secure handling.
* If passwordChars is null but password is set, converts from String.
*
* @return the password as char array, or null if not set
*/
public char[] getPasswordChars() {
if (passwordChars == null && password != null) {
return PasswordSecurityUtil.toCharArray(password);
}
return passwordChars;
}

/**
* Sets the password from a char array (preferred for security).
* Also updates the String password field for backward compatibility.
*
* @param passwordChars the password as char array
*/
public void setPasswordChars(char[] passwordChars) {
this.passwordChars = passwordChars;
if (passwordChars != null) {
this.password = PasswordSecurityUtil.toString(passwordChars);
} else {
this.password = null;
}
}

/**
* Gets the matching password as a char array for secure handling.
* If matchingPasswordChars is null but matchingPassword is set, converts from String.
*
* @return the matching password as char array, or null if not set
*/
public char[] getMatchingPasswordChars() {
if (matchingPasswordChars == null && matchingPassword != null) {
return PasswordSecurityUtil.toCharArray(matchingPassword);
}
return matchingPasswordChars;
}

/**
* Sets the matching password from a char array (preferred for security).
* Also updates the String matchingPassword field for backward compatibility.
*
* @param matchingPasswordChars the matching password as char array
*/
public void setMatchingPasswordChars(char[] matchingPasswordChars) {
this.matchingPasswordChars = matchingPasswordChars;
if (matchingPasswordChars != null) {
this.matchingPassword = PasswordSecurityUtil.toString(matchingPasswordChars);
} else {
this.matchingPassword = null;
}
}

/**
* Clears sensitive password data from memory.
* This method should be called in a finally block to ensure passwords are cleared.
*/
public void clearPasswords() {
PasswordSecurityUtil.clearPassword(passwordChars);
PasswordSecurityUtil.clearPassword(matchingPasswordChars);
passwordChars = null;
matchingPasswordChars = null;
}

/**
* Closes this resource, clearing password data from memory.
*/
@Override
public void close() {
clearPasswords();
}
}
Loading
Loading