Skip to content

Commit 44906bb

Browse files
Copilotdevondragon
andcommitted
Phase 1: Add PasswordSecurityUtil with secure password handling
Co-authored-by: devondragon <1254537+devondragon@users.noreply.github.com>
1 parent cffebf6 commit 44906bb

2 files changed

Lines changed: 332 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.digitalsanctuary.spring.user.util;
2+
3+
import java.util.Arrays;
4+
5+
/**
6+
* Utility class for secure password handling to minimize password exposure in memory.
7+
*
8+
* <p>This class provides methods for:
9+
* <ul>
10+
* <li>Constant-time password comparison to prevent timing attacks</li>
11+
* <li>Safe conversion between char[] and String when required</li>
12+
* <li>Explicit memory clearing of sensitive data</li>
13+
* </ul>
14+
*
15+
* <p><strong>Security Rationale:</strong>
16+
* <ul>
17+
* <li>char[] arrays can be explicitly cleared from memory after use</li>
18+
* <li>String objects are immutable and remain in memory until garbage collected</li>
19+
* <li>Using char[] reduces password exposure time in memory</li>
20+
* </ul>
21+
*
22+
* @author SpringUserFramework
23+
* @since 3.1.0
24+
*/
25+
public final class PasswordSecurityUtil {
26+
27+
/**
28+
* Private constructor to prevent instantiation.
29+
*/
30+
private PasswordSecurityUtil() {
31+
throw new UnsupportedOperationException("Utility class should not be instantiated");
32+
}
33+
34+
/**
35+
* Compares two char[] arrays in constant time to prevent timing attacks.
36+
*
37+
* <p>This method always compares the full length of both arrays to avoid
38+
* leaking information about password length or content through timing.
39+
*
40+
* @param password1 first password array
41+
* @param password2 second password array
42+
* @return true if both arrays are non-null and contain the same characters
43+
*/
44+
public static boolean constantTimeEquals(char[] password1, char[] password2) {
45+
if (password1 == null || password2 == null) {
46+
return password1 == password2;
47+
}
48+
49+
// Different lengths - always return false but continue timing to avoid leak
50+
if (password1.length != password2.length) {
51+
// Still perform a comparison to maintain constant time
52+
int diff = 0;
53+
for (int i = 0; i < password1.length && i < password2.length; i++) {
54+
diff |= password1[i] ^ password2[i];
55+
}
56+
return false;
57+
}
58+
59+
// Same length - compare all characters
60+
int diff = 0;
61+
for (int i = 0; i < password1.length; i++) {
62+
diff |= password1[i] ^ password2[i];
63+
}
64+
65+
return diff == 0;
66+
}
67+
68+
/**
69+
* Securely clears a char[] array by overwriting with zeros.
70+
*
71+
* <p>This method should be called in a finally block to ensure
72+
* sensitive data is cleared even if an exception occurs.
73+
*
74+
* @param password the password array to clear (can be null)
75+
*/
76+
public static void clearPassword(char[] password) {
77+
if (password != null) {
78+
Arrays.fill(password, '\0');
79+
}
80+
}
81+
82+
/**
83+
* Converts a char[] to String when required by libraries.
84+
*
85+
* <p><strong>Important:</strong> The resulting String cannot be cleared
86+
* from memory until garbage collected. Use this method only when necessary
87+
* and clear the char[] array immediately after conversion.
88+
*
89+
* @param password the password char array
90+
* @return String representation of the password, or null if input is null
91+
*/
92+
public static String toString(char[] password) {
93+
if (password == null) {
94+
return null;
95+
}
96+
return new String(password);
97+
}
98+
99+
/**
100+
* Converts a String to char[] for secure handling.
101+
*
102+
* <p>Note: The original String will still remain in memory until
103+
* garbage collected. This method is primarily useful for transitioning
104+
* existing String-based code to char[]-based handling.
105+
*
106+
* @param password the password string
107+
* @return char array representation, or null if input is null
108+
*/
109+
public static char[] toCharArray(String password) {
110+
if (password == null) {
111+
return null;
112+
}
113+
return password.toCharArray();
114+
}
115+
116+
/**
117+
* Validates that a char[] password is not null and not empty.
118+
*
119+
* @param password the password to validate
120+
* @return true if password is non-null and has length > 0
121+
*/
122+
public static boolean isNotEmpty(char[] password) {
123+
return password != null && password.length > 0;
124+
}
125+
126+
/**
127+
* Validates that a char[] password is null or empty.
128+
*
129+
* @param password the password to validate
130+
* @return true if password is null or has length 0
131+
*/
132+
public static boolean isEmpty(char[] password) {
133+
return password == null || password.length == 0;
134+
}
135+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package com.digitalsanctuary.spring.user.util;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
/**
8+
* Unit tests for PasswordSecurityUtil.
9+
*/
10+
class PasswordSecurityUtilTest {
11+
12+
@Test
13+
void constantTimeEquals_returnsTrue_whenBothNull() {
14+
assertTrue(PasswordSecurityUtil.constantTimeEquals(null, null));
15+
}
16+
17+
@Test
18+
void constantTimeEquals_returnsFalse_whenOneNull() {
19+
assertFalse(PasswordSecurityUtil.constantTimeEquals(null, "password".toCharArray()));
20+
assertFalse(PasswordSecurityUtil.constantTimeEquals("password".toCharArray(), null));
21+
}
22+
23+
@Test
24+
void constantTimeEquals_returnsTrue_whenIdentical() {
25+
char[] password1 = "MySecureP@ssw0rd".toCharArray();
26+
char[] password2 = "MySecureP@ssw0rd".toCharArray();
27+
assertTrue(PasswordSecurityUtil.constantTimeEquals(password1, password2));
28+
}
29+
30+
@Test
31+
void constantTimeEquals_returnsFalse_whenDifferent() {
32+
char[] password1 = "MySecureP@ssw0rd".toCharArray();
33+
char[] password2 = "DifferentP@ss".toCharArray();
34+
assertFalse(PasswordSecurityUtil.constantTimeEquals(password1, password2));
35+
}
36+
37+
@Test
38+
void constantTimeEquals_returnsFalse_whenDifferentLength() {
39+
char[] password1 = "short".toCharArray();
40+
char[] password2 = "muchlongerpassword".toCharArray();
41+
assertFalse(PasswordSecurityUtil.constantTimeEquals(password1, password2));
42+
}
43+
44+
@Test
45+
void constantTimeEquals_returnsFalse_whenOneCharacterDifferent() {
46+
char[] password1 = "MySecureP@ssw0rd".toCharArray();
47+
char[] password2 = "MySecureP@ssw0rD".toCharArray(); // last char different
48+
assertFalse(PasswordSecurityUtil.constantTimeEquals(password1, password2));
49+
}
50+
51+
@Test
52+
void constantTimeEquals_returnsTrue_whenBothEmpty() {
53+
char[] password1 = new char[0];
54+
char[] password2 = new char[0];
55+
assertTrue(PasswordSecurityUtil.constantTimeEquals(password1, password2));
56+
}
57+
58+
@Test
59+
void clearPassword_clearsArray() {
60+
char[] password = "MySecureP@ssw0rd".toCharArray();
61+
PasswordSecurityUtil.clearPassword(password);
62+
63+
// Verify all characters are cleared to '\0'
64+
for (char c : password) {
65+
assertEquals('\0', c, "Password should be cleared to null characters");
66+
}
67+
}
68+
69+
@Test
70+
void clearPassword_handlesNull() {
71+
// Should not throw exception
72+
assertDoesNotThrow(() -> PasswordSecurityUtil.clearPassword(null));
73+
}
74+
75+
@Test
76+
void clearPassword_handlesEmptyArray() {
77+
char[] password = new char[0];
78+
assertDoesNotThrow(() -> PasswordSecurityUtil.clearPassword(password));
79+
}
80+
81+
@Test
82+
void toString_convertsCorrectly() {
83+
char[] password = "MySecureP@ssw0rd".toCharArray();
84+
String result = PasswordSecurityUtil.toString(password);
85+
assertEquals("MySecureP@ssw0rd", result);
86+
}
87+
88+
@Test
89+
void toString_handlesNull() {
90+
assertNull(PasswordSecurityUtil.toString(null));
91+
}
92+
93+
@Test
94+
void toString_handlesEmptyArray() {
95+
char[] password = new char[0];
96+
String result = PasswordSecurityUtil.toString(password);
97+
assertEquals("", result);
98+
}
99+
100+
@Test
101+
void toCharArray_convertsCorrectly() {
102+
String password = "MySecureP@ssw0rd";
103+
char[] result = PasswordSecurityUtil.toCharArray(password);
104+
assertArrayEquals("MySecureP@ssw0rd".toCharArray(), result);
105+
}
106+
107+
@Test
108+
void toCharArray_handlesNull() {
109+
assertNull(PasswordSecurityUtil.toCharArray(null));
110+
}
111+
112+
@Test
113+
void toCharArray_handlesEmptyString() {
114+
String password = "";
115+
char[] result = PasswordSecurityUtil.toCharArray(password);
116+
assertNotNull(result);
117+
assertEquals(0, result.length);
118+
}
119+
120+
@Test
121+
void isNotEmpty_returnsTrue_whenNotEmpty() {
122+
char[] password = "password".toCharArray();
123+
assertTrue(PasswordSecurityUtil.isNotEmpty(password));
124+
}
125+
126+
@Test
127+
void isNotEmpty_returnsFalse_whenNull() {
128+
assertFalse(PasswordSecurityUtil.isNotEmpty(null));
129+
}
130+
131+
@Test
132+
void isNotEmpty_returnsFalse_whenEmpty() {
133+
char[] password = new char[0];
134+
assertFalse(PasswordSecurityUtil.isNotEmpty(password));
135+
}
136+
137+
@Test
138+
void isEmpty_returnsTrue_whenNull() {
139+
assertTrue(PasswordSecurityUtil.isEmpty(null));
140+
}
141+
142+
@Test
143+
void isEmpty_returnsTrue_whenEmpty() {
144+
char[] password = new char[0];
145+
assertTrue(PasswordSecurityUtil.isEmpty(password));
146+
}
147+
148+
@Test
149+
void isEmpty_returnsFalse_whenNotEmpty() {
150+
char[] password = "password".toCharArray();
151+
assertFalse(PasswordSecurityUtil.isEmpty(password));
152+
}
153+
154+
@Test
155+
void constructor_throwsException() {
156+
assertThrows(java.lang.reflect.InvocationTargetException.class, () -> {
157+
// Use reflection to try to instantiate
158+
java.lang.reflect.Constructor<PasswordSecurityUtil> constructor =
159+
PasswordSecurityUtil.class.getDeclaredConstructor();
160+
constructor.setAccessible(true);
161+
constructor.newInstance();
162+
});
163+
}
164+
165+
@Test
166+
void integrationTest_securePasswordFlow() {
167+
// Simulate a secure password handling flow
168+
String userInput = "MySecureP@ssw0rd!";
169+
170+
// 1. Convert to char[]
171+
char[] password = PasswordSecurityUtil.toCharArray(userInput);
172+
assertNotNull(password);
173+
174+
// 2. Validate
175+
assertTrue(PasswordSecurityUtil.isNotEmpty(password));
176+
177+
// 3. Compare with another password
178+
char[] matchingPassword = "MySecureP@ssw0rd!".toCharArray();
179+
assertTrue(PasswordSecurityUtil.constantTimeEquals(password, matchingPassword));
180+
181+
// 4. Convert to String when needed (e.g., for PasswordEncoder)
182+
String passwordString = PasswordSecurityUtil.toString(password);
183+
assertEquals(userInput, passwordString);
184+
185+
// 5. Clear both arrays
186+
PasswordSecurityUtil.clearPassword(password);
187+
PasswordSecurityUtil.clearPassword(matchingPassword);
188+
189+
// 6. Verify cleared
190+
for (char c : password) {
191+
assertEquals('\0', c);
192+
}
193+
for (char c : matchingPassword) {
194+
assertEquals('\0', c);
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)