SEC-1666: Use constant time comparison for sensitive data.

Constant time comparison helps to mitigate timing attacks. See the following link for more information

 * http://rdist.root.org/2010/07/19/exploiting-remote-timing-attacks/
 * http://en.wikipedia.org/wiki/Timing_attack for more information.
This commit is contained in:
Rob Winch 2011-01-31 23:00:16 -06:00
parent 6a62b51870
commit 8c08eeb57b
9 changed files with 145 additions and 13 deletions

View File

@ -145,7 +145,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
String encodedRawPass = encodePassword(rawPass, salt).substring(startOfHash); String encodedRawPass = encodePassword(rawPass, salt).substring(startOfHash);
return encodedRawPass.equals(encPass.substring(startOfHash)); return PasswordEncoderUtils.equals(encodedRawPass,encPass.substring(startOfHash));
} }
/** /**

View File

@ -78,7 +78,7 @@ public class Md4PasswordEncoder extends BaseDigestPasswordEncoder {
public boolean isPasswordValid(String encPass, String rawPass, Object salt) { public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
String pass1 = "" + encPass; String pass1 = "" + encPass;
String pass2 = encodePassword(rawPass, salt); String pass2 = encodePassword(rawPass, salt);
return pass1.equals(pass2); return PasswordEncoderUtils.equals(pass1,pass2);
} }
public String getAlgorithm() { public String getAlgorithm() {

View File

@ -126,7 +126,7 @@ public class MessageDigestPasswordEncoder extends BaseDigestPasswordEncoder {
String pass1 = "" + encPass; String pass1 = "" + encPass;
String pass2 = encodePassword(rawPass, salt); String pass2 = encodePassword(rawPass, salt);
return pass1.equals(pass2); return PasswordEncoderUtils.equals(pass1,pass2);
} }
public String getAlgorithm() { public String getAlgorithm() {

View File

@ -0,0 +1,45 @@
package org.springframework.security.authentication.encoding;
import java.io.UnsupportedEncodingException;
/**
* Utility for constant time comparison to prevent against timing attacks.
*
* @author Rob Winch
*/
class PasswordEncoderUtils {
/**
* Constant time comparison to prevent against timing attacks.
* @param expected
* @param actual
* @return
*/
static boolean equals(String expected, String actual) {
byte[] expectedBytes = bytesUtf8(expected);
byte[] actualBytes = bytesUtf8(actual);
int expectedLength = expectedBytes == null ? -1 : expectedBytes.length;
int actualLength = actualBytes == null ? -1 : actualBytes.length;
if (expectedLength != actualLength) {
return false;
}
int result = 0;
for (int i = 0; i < expectedLength; i++) {
result |= expectedBytes[i] ^ actualBytes[i];
}
return result == 0;
}
private static byte[] bytesUtf8(String s) {
if(s == null) {
return null;
}
try {
return s.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Could not get bytes in UTF-8 format",e);
}
}
private PasswordEncoderUtils() {}
}

View File

@ -15,6 +15,8 @@
package org.springframework.security.authentication.encoding; package org.springframework.security.authentication.encoding;
import java.util.Locale;
/** /**
* <p>Plaintext implementation of PasswordEncoder.</p> * <p>Plaintext implementation of PasswordEncoder.</p>
* <P>As callers may wish to extract the password and salts separately from the encoded password, the salt must * <P>As callers may wish to extract the password and salts separately from the encoded password, the salt must
@ -46,11 +48,12 @@ public class PlaintextPasswordEncoder extends BasePasswordEncoder {
// authentication will fail as the encodePassword never allows them) // authentication will fail as the encodePassword never allows them)
String pass2 = mergePasswordAndSalt(rawPass, salt, false); String pass2 = mergePasswordAndSalt(rawPass, salt, false);
if (!ignorePasswordCase) { if (ignorePasswordCase) {
return pass1.equals(pass2); // Note: per String javadoc to get correct results for Locale insensitive, use English
} else { pass1 = pass1.toLowerCase(Locale.ENGLISH);
return pass1.equalsIgnoreCase(pass2); pass2 = pass2.toLowerCase(Locale.ENGLISH);
} }
return PasswordEncoderUtils.equals(pass1,pass2);
} }
/** /**

View File

@ -0,0 +1,33 @@
package org.springframework.security.authentication.encoding;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* @author Rob Winch
*/
public class PasswordEncoderUtilsTests {
@Test
public void differentLength() {
assertFalse(PasswordEncoderUtils.equals("abc", "a"));
assertFalse(PasswordEncoderUtils.equals("a", "abc"));
}
@Test
public void equalsNull() {
assertFalse(PasswordEncoderUtils.equals(null, "a"));
assertFalse(PasswordEncoderUtils.equals("a", null));
assertTrue(PasswordEncoderUtils.equals(null, null));
}
@Test
public void equalsCaseSensitive() {
assertFalse(PasswordEncoderUtils.equals("aBc", "abc"));
}
@Test
public void equalsSuccess() {
assertTrue(PasswordEncoderUtils.equals("abcdef", "abcdef"));
}
}

View File

@ -21,8 +21,6 @@ import static org.springframework.security.crypto.util.EncodingUtils.hexEncode;
import static org.springframework.security.crypto.util.EncodingUtils.subArray; import static org.springframework.security.crypto.util.EncodingUtils.subArray;
import static org.springframework.security.crypto.util.EncodingUtils.utf8Encode; import static org.springframework.security.crypto.util.EncodingUtils.utf8Encode;
import java.util.Arrays;
import org.springframework.security.crypto.keygen.BytesKeyGenerator; import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.util.Digester; import org.springframework.security.crypto.util.Digester;
@ -79,8 +77,21 @@ public final class StandardPasswordEncoder implements PasswordEncoder {
return hexDecode(encodedPassword); return hexDecode(encodedPassword);
} }
/**
* Constant time comparison to prevent against timing attacks.
* @param expected
* @param actual
* @return
*/
private boolean matches(byte[] expected, byte[] actual) { private boolean matches(byte[] expected, byte[] actual) {
return Arrays.equals(expected, actual); if (expected.length != actual.length) {
} return false;
}
int result = 0;
for (int i = 0; i < expected.length; i++) {
result |= expected[i] ^ actual[i];
}
return result == 0;
}
} }

View File

@ -16,6 +16,12 @@ public class StandardPasswordEncoderTests {
assertTrue(encoder.matches("password", result)); assertTrue(encoder.matches("password", result));
} }
@Test
public void matchesLengthChecked() {
String result = encoder.encode("password");
assertFalse(encoder.matches("password", result.substring(0,result.length()-1)));
}
@Test @Test
public void notMatches() { public void notMatches() {
String result = encoder.encode("password"); String result = encoder.encode("password");

View File

@ -23,6 +23,7 @@ import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Arrays; import java.util.Arrays;
@ -81,6 +82,7 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
//~ Methods ======================================================================================================== //~ Methods ========================================================================================================
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) { HttpServletResponse response) {
@ -117,9 +119,9 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(), String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
userDetails.getPassword()); userDetails.getPassword());
if (!expectedTokenSignature.equals(cookieTokens[2])) { if (!equals(expectedTokenSignature,cookieTokens[2])) {
throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2] throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
+ "' but expected '" + expectedTokenSignature + "'"); + "' but expected '" + expectedTokenSignature + "'");
} }
return userDetails; return userDetails;
@ -145,6 +147,7 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
return tokenExpiryTime < System.currentTimeMillis(); return tokenExpiryTime < System.currentTimeMillis();
} }
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) { Authentication successfulAuthentication) {
@ -216,4 +219,35 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
private boolean isInstanceOfUserDetails(Authentication authentication) { private boolean isInstanceOfUserDetails(Authentication authentication) {
return authentication.getPrincipal() instanceof UserDetails; return authentication.getPrincipal() instanceof UserDetails;
} }
/**
* Constant time comparison to prevent against timing attacks.
* @param expected
* @param actual
* @return
*/
private static boolean equals(String expected, String actual) {
byte[] expectedBytes = bytesUtf8(expected);
byte[] actualBytes = bytesUtf8(actual);
if (expectedBytes.length != actualBytes.length) {
return false;
}
int result = 0;
for (int i = 0; i < expectedBytes.length; i++) {
result |= expectedBytes[i] ^ actualBytes[i];
}
return result == 0;
}
private static byte[] bytesUtf8(String s) {
if(s == null) {
return null;
}
try {
return s.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Could not get bytes in UTF-8 format",e);
}
}
} }