From 67c9a0b78dce7536bfa2f50019f63f4ce9d3c03f Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sat, 6 Feb 2010 17:34:07 +0000 Subject: [PATCH] SEC-1389: Added "iterations" property to BaseDigestpasswordEncoder to support "stretching" of passwords. --- .../MessageDigestPasswordEncoder.java | 39 +++++++++++++++---- .../encoding/Md5PasswordEncoderTests.java | 21 ++++++++-- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authentication/encoding/MessageDigestPasswordEncoder.java b/core/src/main/java/org/springframework/security/authentication/encoding/MessageDigestPasswordEncoder.java index a306a19be9..cda94945fb 100644 --- a/core/src/main/java/org/springframework/security/authentication/encoding/MessageDigestPasswordEncoder.java +++ b/core/src/main/java/org/springframework/security/authentication/encoding/MessageDigestPasswordEncoder.java @@ -6,34 +6,39 @@ import java.security.NoSuchAlgorithmException; import org.springframework.security.core.codec.Base64; import org.springframework.security.core.codec.Hex; +import org.springframework.util.Assert; /** * Base for digest password encoders. - *

This class can be used stand-alone, or one of the subclasses can be used for compatiblity and convenience. + *

+ * This class can be used stand-alone, or one of the subclasses can be used for compatiblity and convenience. * When using this class directly you must specify a - * - * Message Digest Algorithm to use as a constructor arg

- * - *

The encoded password hash is normally returned as Hex (32 char) version of the hash bytes. + * + * Message Digest Algorithm to use as a constructor arg. + *

+ * The encoded password hash is normally returned as Hex (32 char) version of the hash bytes. * Setting the encodeHashAsBase64 property to true will cause the encoded pass to be returned * as Base64 text, which will consume 24 characters. * See {@link BaseDigestPasswordEncoder#setEncodeHashAsBase64(boolean)} - *

*

- * This PasswordEncoder can be used directly as in the following example:
+ * This {@code PasswordEncoder} can be used directly as in the following example: *

  * <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.MessageDigestPasswordEncoder">
  *     <constructor-arg value="MD5"/>
  * </bean>
  * 
- *

+ *

+ * If desired, the {@link #setIterations iterations} property can be set to enable + * "password stretching" for the digest calculation. * * @author Ray Krueger + * @author Luke Taylor * @since 1.0.1 */ public class MessageDigestPasswordEncoder extends BaseDigestPasswordEncoder { private final String algorithm; + private int iterations = 1; /** * The digest algorithm to use @@ -81,6 +86,11 @@ public class MessageDigestPasswordEncoder extends BaseDigestPasswordEncoder { throw new IllegalStateException("UTF-8 not supported!"); } + // "stretch" the encoded value if configured to do so + for (int i = 1; i < iterations; i++) { + digest = messageDigest.digest(digest); + } + if (getEncodeHashAsBase64()) { return new String(Base64.encode(digest)); } else { @@ -122,4 +132,17 @@ public class MessageDigestPasswordEncoder extends BaseDigestPasswordEncoder { public String getAlgorithm() { return algorithm; } + + /** + * Sets the number of iterations for which the calculated hash value should be "stretched". If this is greater + * than one, the initial digest is calculated, the digest function will be called repeatedly on the result for + * the additional number of iterations. + * + * @param iterations the number of iterations which will be executed on the hashed password/salt + * value. Defaults to 1. + */ + public void setIterations(int iterations) { + Assert.isTrue(iterations > 0, "Iterations value must be greater than zero"); + this.iterations = iterations; + } } diff --git a/core/src/test/java/org/springframework/security/authentication/encoding/Md5PasswordEncoderTests.java b/core/src/test/java/org/springframework/security/authentication/encoding/Md5PasswordEncoderTests.java index bb66604f1e..06f849bf02 100644 --- a/core/src/test/java/org/springframework/security/authentication/encoding/Md5PasswordEncoderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/encoding/Md5PasswordEncoderTests.java @@ -15,9 +15,9 @@ package org.springframework.security.authentication.encoding; -import org.springframework.security.authentication.encoding.Md5PasswordEncoder; +import static org.junit.Assert.*; -import junit.framework.TestCase; +import org.junit.Test; /** @@ -26,10 +26,12 @@ import junit.framework.TestCase; * @author colin sampaleanu * @author Ben Alex * @author Ray Krueger + * @author Luke Taylor */ -public class Md5PasswordEncoderTests extends TestCase { +public class Md5PasswordEncoderTests { //~ Methods ======================================================================================================== + @Test public void testBasicFunctionality() { Md5PasswordEncoder pe = new Md5PasswordEncoder(); String raw = "abc123"; @@ -42,12 +44,14 @@ public class Md5PasswordEncoderTests extends TestCase { assertEquals("MD5", pe.getAlgorithm()); } - public void testNonAsciiPasswordHasCorrectHash() { + @Test + public void nonAsciiPasswordHasCorrectHash() { Md5PasswordEncoder md5 = new Md5PasswordEncoder(); String encodedPassword = md5.encodePassword("\u4F60\u597d", null); assertEquals("7eca689f0d3389d9dea66ae112e5cfd7", encodedPassword); } + @Test public void testBase64() throws Exception { Md5PasswordEncoder pe = new Md5PasswordEncoder(); pe.setEncodeHashAsBase64(true); @@ -59,4 +63,13 @@ public class Md5PasswordEncoderTests extends TestCase { assertFalse(pe.isPasswordValid(encoded, badRaw, salt)); assertTrue(encoded.length() != 32); } + + @Test + public void stretchFactorIsProcessedCorrectly() throws Exception { + Md5PasswordEncoder pe = new Md5PasswordEncoder(); + pe.setIterations(2); + // Calculate value using: + // echo -n password{salt} | openssl md5 -binary | openssl md5 + assertEquals("eb753fb0c370582b4ee01b30f304b9fc", pe.encodePassword("password", "salt")); + } }