Add Utf8#isEqual
This commit adds a constant-time equals method, useful for comparing password hashes or other sensitive material Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,9 @@ import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* UTF-8 Charset encoder/decoder.
|
||||
@@ -66,4 +69,21 @@ public final class Utf8 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant time comparison to prevent against timing attacks.
|
||||
* @param expected the expected {@link CharSequence}
|
||||
* @param actual the actual {@link CharSequence}
|
||||
* @return true if {@code expected} and {@code actual} are equal, false otherwise
|
||||
* @since 5.7.26
|
||||
*/
|
||||
public static boolean isEqual(@Nullable CharSequence expected, @Nullable CharSequence actual) {
|
||||
byte[] expectedBytes = bytesUtf8(expected);
|
||||
byte[] actualBytes = bytesUtf8(actual);
|
||||
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
||||
}
|
||||
|
||||
private static byte @Nullable [] bytesUtf8(@Nullable CharSequence s) {
|
||||
return (s != null) ? Utf8.encode(s) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-11
@@ -16,8 +16,6 @@
|
||||
|
||||
package org.springframework.security.crypto.password;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
@@ -39,15 +37,7 @@ final class PasswordEncoderUtils {
|
||||
* @return
|
||||
*/
|
||||
static boolean equals(String expected, @Nullable String actual) {
|
||||
byte[] expectedBytes = bytesUtf8(expected);
|
||||
byte[] actualBytes = bytesUtf8(actual);
|
||||
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
||||
}
|
||||
|
||||
private static byte @Nullable [] bytesUtf8(@Nullable String s) {
|
||||
// need to check if Utf8.encode() runs in constant time (probably not).
|
||||
// This may leak length of string.
|
||||
return (s != null) ? Utf8.encode(s) : null;
|
||||
return Utf8.isEqual(expected, actual);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,4 +37,48 @@ public class Utf8Tests {
|
||||
assertThat(decoded).isEqualTo("6048b75ed560785c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenDifferentLengthThenFalse() {
|
||||
assertThat(Utf8.isEqual("abc", "a")).isFalse();
|
||||
assertThat(Utf8.isEqual("a", "abc")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenNullAndNotEmptyThenFalse() {
|
||||
assertThat(Utf8.isEqual(null, "a")).isFalse();
|
||||
assertThat(Utf8.isEqual("a", null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenNullAndNullThenTrue() {
|
||||
assertThat(Utf8.isEqual(null, null)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenNullAndEmptyThenFalse() {
|
||||
assertThat(Utf8.isEqual(null, "")).isFalse();
|
||||
assertThat(Utf8.isEqual("", null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenNotEmptyAndEmptyThenFalse() {
|
||||
assertThat(Utf8.isEqual("abc", "")).isFalse();
|
||||
assertThat(Utf8.isEqual("", "abc")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenEmptyAndEmptyThenTrue() {
|
||||
assertThat(Utf8.isEqual("", "")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenDifferentCaseThenFalse() {
|
||||
assertThat(Utf8.isEqual("aBc", "abc")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenSameThenTrue() {
|
||||
assertThat(Utf8.isEqual("abcdef", "abcdef")).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user