From 00620b6992754022adf2fad90b0eecdc9e4b49d0 Mon Sep 17 00:00:00 2001 From: Ray Krueger Date: Wed, 31 May 2006 03:03:18 +0000 Subject: [PATCH] http://opensource.atlassian.com/projects/spring/browse/SEC-96 Refactored Digest encoding for better support of all MessageDigest algorithms, such as the SHA family. --- .../encoding/Md5PasswordEncoder.java | 35 ++---- .../MessageDigestPasswordEncoder.java | 114 ++++++++++++++++++ .../encoding/ShaPasswordEncoder.java | 50 ++++---- .../encoding/Md5PasswordEncoderTests.java | 13 +- .../encoding/ShaPasswordEncoderTests.java | 30 ++++- 5 files changed, 186 insertions(+), 56 deletions(-) create mode 100644 core/src/main/java/org/acegisecurity/providers/encoding/MessageDigestPasswordEncoder.java diff --git a/core/src/main/java/org/acegisecurity/providers/encoding/Md5PasswordEncoder.java b/core/src/main/java/org/acegisecurity/providers/encoding/Md5PasswordEncoder.java index 3da97c7324..bdd7da4644 100644 --- a/core/src/main/java/org/acegisecurity/providers/encoding/Md5PasswordEncoder.java +++ b/core/src/main/java/org/acegisecurity/providers/encoding/Md5PasswordEncoder.java @@ -12,42 +12,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.acegisecurity.providers.encoding; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.digest.DigestUtils; - - /** *

MD5 implementation of PasswordEncoder.

- *

If a null password is presented, it will be treated as an empty String ("") + *

If a null password is presented, it will be treated as an empty String ("") * password.

- *

As MD5 is a one-way hash, the salt can contain any characters.

+ *

As MD5 is a one-way hash, the salt can contain any characters.

* + * This is a convenience class that extends the + * {@link MessageDigestPasswordEncoder} and passes MD5 as the algorithm to use. + * + * @author Ray Krueger * @author colin sampaleanu * @author Ben Alex * @version $Id$ */ -public class Md5PasswordEncoder extends BaseDigestPasswordEncoder implements PasswordEncoder { - //~ Methods ======================================================================================================== +public class Md5PasswordEncoder extends MessageDigestPasswordEncoder { - public String encodePassword(String rawPass, Object salt) { - String saltedPass = mergePasswordAndSalt(rawPass, salt, false); - - if (!getEncodeHashAsBase64()) { - return DigestUtils.md5Hex(saltedPass); - } - - byte[] encoded = Base64.encodeBase64(DigestUtils.md5(saltedPass)); - - return new String(encoded); - } - - public boolean isPasswordValid(String encPass, String rawPass, Object salt) { - String pass1 = "" + encPass; - String pass2 = encodePassword(rawPass, salt); - - return pass1.equals(pass2); + public Md5PasswordEncoder() { + super("MD5"); } } diff --git a/core/src/main/java/org/acegisecurity/providers/encoding/MessageDigestPasswordEncoder.java b/core/src/main/java/org/acegisecurity/providers/encoding/MessageDigestPasswordEncoder.java new file mode 100644 index 0000000000..4e98a83b2c --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/encoding/MessageDigestPasswordEncoder.java @@ -0,0 +1,114 @@ +package org.acegisecurity.providers.encoding; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + *

Base for digest password encoders.

+ * 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. 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:
+ * <bean id="passwordEncoder" class="org.acegisecurity.providers.encoding.MessageDigestPasswordEncoder">
+ *   <constructor-arg value="MD5"/>
+ * </bean> + * + * + * @author Ray Krueger + * @since 1.0.1 + */ +public class MessageDigestPasswordEncoder extends BaseDigestPasswordEncoder { + + private final String algorithm; + + /** + * The digest algorithm to use + * Supports the named + * Message Digest Algorithms in the Java environment. + * + * @param algorithm + */ + public MessageDigestPasswordEncoder(String algorithm) { + this(algorithm, false); + } + + /** + * Convenience constructor for specifying the algorithm and whether or not to enable base64 encoding + * + * @param algorithm + * @param encodeHashAsBase64 + * @throws IllegalArgumentException if an unknown + */ + public MessageDigestPasswordEncoder(String algorithm, boolean encodeHashAsBase64) throws IllegalArgumentException { + this.algorithm = algorithm; + setEncodeHashAsBase64(encodeHashAsBase64); + //Validity Check + getMessageDigest(); + } + + /** + * Encodes the rawPass using a MessageDigest. + * If a salt is specified it will be merged with the password before encoding. + * + * @param rawPass The plain text password + * @param salt The salt to sprinkle + * @return Hex string of password digest (or base64 encoded string if encodeHashAsBase64 is enabled. + */ + public String encodePassword(String rawPass, Object salt) { + String saltedPass = mergePasswordAndSalt(rawPass, salt, false); + + MessageDigest messageDigest = getMessageDigest(); + + byte[] digest = messageDigest.digest(saltedPass.getBytes()); + + if (getEncodeHashAsBase64()) { + return new String(Base64.encodeBase64(digest)); + } else { + return new String(Hex.encodeHex(digest)); + } + } + + /** + * Get a MessageDigest instance for the given algorithm. + * Throws an IllegalArgumentException if algorithm is unknown + * + * @return MessageDigest instance + * @throws IllegalArgumentException if NoSuchAlgorithmException is thrown + */ + protected final MessageDigest getMessageDigest() throws IllegalArgumentException { + try { + return MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("No such algorithm [" + + algorithm + "]"); + } + } + + /** + * Takes a previously encoded password and compares it with a rawpassword after mixing in the salt and + * encoding that value + * + * @param encPass previously encoded password + * @param rawPass plain text password + * @param salt salt to mix into password + * @return true or false + */ + public boolean isPasswordValid(String encPass, String rawPass, Object salt) { + String pass1 = "" + encPass; + String pass2 = encodePassword(rawPass, salt); + + return pass1.equals(pass2); + } + + public String getAlgorithm() { + return algorithm; + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/encoding/ShaPasswordEncoder.java b/core/src/main/java/org/acegisecurity/providers/encoding/ShaPasswordEncoder.java index 808d06ccfe..e481e00645 100644 --- a/core/src/main/java/org/acegisecurity/providers/encoding/ShaPasswordEncoder.java +++ b/core/src/main/java/org/acegisecurity/providers/encoding/ShaPasswordEncoder.java @@ -12,42 +12,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.acegisecurity.providers.encoding; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.digest.DigestUtils; - - /** *

SHA implementation of PasswordEncoder.

- *

If a null password is presented, it will be treated as an empty String ("") + *

If a null password is presented, it will be treated as an empty String ("") * password.

- *

As SHA is a one-way hash, the salt can contain any characters.

+ *

As SHA is a one-way hash, the salt can contain any characters.

* + * The default strength for the SHA encoding is SHA-1. If you wish to use higher strengths use the argumented constructor. + * {@link #ShaPasswordEncoder(int strength)} + *
+ * The applicationContext example...
+ * <bean id="passwordEncoder" class="org.acegisecurity.providers.encoding.ShaPasswordEncoder">
+ *   <constructor-arg value="256"/>
+ * </bean> + * + * + * @author Ray Krueger * @author colin sampaleanu * @author Ben Alex * @version $Id$ */ -public class ShaPasswordEncoder extends BaseDigestPasswordEncoder implements PasswordEncoder { - //~ Methods ======================================================================================================== +public class ShaPasswordEncoder extends MessageDigestPasswordEncoder { - public String encodePassword(String rawPass, Object salt) { - String saltedPass = mergePasswordAndSalt(rawPass, salt, false); - - if (!getEncodeHashAsBase64()) { - return DigestUtils.shaHex(saltedPass); - } - - byte[] encoded = Base64.encodeBase64(DigestUtils.sha(saltedPass)); - - return new String(encoded); + /** + * Initializes the ShaPasswordEncoder for SHA-1 strength + */ + public ShaPasswordEncoder() { + this(1); } - public boolean isPasswordValid(String encPass, String rawPass, Object salt) { - String pass1 = "" + encPass; - String pass2 = encodePassword(rawPass, salt); - - return pass1.equals(pass2); + /** + * Initialize the ShaPasswordEncoder with a given SHA stength as supported by the JVM + * EX: ShaPasswordEncoder encoder = new ShaPasswordEncoder(256); initializes with SHA-256 + * + * @param strength EX: 1, 256, 384, 512 + */ + public ShaPasswordEncoder(int strength) { + super("SHA-" + strength); } } diff --git a/core/src/test/java/org/acegisecurity/providers/encoding/Md5PasswordEncoderTests.java b/core/src/test/java/org/acegisecurity/providers/encoding/Md5PasswordEncoderTests.java index 88dc9dc3a1..1ee0d2d95d 100644 --- a/core/src/test/java/org/acegisecurity/providers/encoding/Md5PasswordEncoderTests.java +++ b/core/src/test/java/org/acegisecurity/providers/encoding/Md5PasswordEncoderTests.java @@ -23,6 +23,7 @@ import junit.framework.TestCase; * * @author colin sampaleanu * @author Ben Alex + * @author Ray Krueger * @version $Id$ */ public class Md5PasswordEncoderTests extends TestCase { @@ -36,11 +37,17 @@ public class Md5PasswordEncoderTests extends TestCase { String encoded = pe.encodePassword(raw, salt); assertTrue(pe.isPasswordValid(encoded, raw, salt)); assertFalse(pe.isPasswordValid(encoded, badRaw, salt)); - assertTrue(encoded.length() == 32); + assertEquals("a68aafd90299d0b137de28fb4bb68573", encoded); + assertEquals("MD5", pe.getAlgorithm()); + } - // now try Base64 + public void testBase64() throws Exception { + Md5PasswordEncoder pe = new Md5PasswordEncoder(); pe.setEncodeHashAsBase64(true); - encoded = pe.encodePassword(raw, salt); + String raw = "abc123"; + String badRaw = "abc321"; + String salt = "THIS_IS_A_SALT"; + String encoded = pe.encodePassword(raw, salt); assertTrue(pe.isPasswordValid(encoded, raw, salt)); assertFalse(pe.isPasswordValid(encoded, badRaw, salt)); assertTrue(encoded.length() != 32); diff --git a/core/src/test/java/org/acegisecurity/providers/encoding/ShaPasswordEncoderTests.java b/core/src/test/java/org/acegisecurity/providers/encoding/ShaPasswordEncoderTests.java index 13d1612d27..5e2f96eb9c 100644 --- a/core/src/test/java/org/acegisecurity/providers/encoding/ShaPasswordEncoderTests.java +++ b/core/src/test/java/org/acegisecurity/providers/encoding/ShaPasswordEncoderTests.java @@ -23,6 +23,7 @@ import junit.framework.TestCase; * * @author colin sampaleanu * @author Ben Alex + * @author Ray Krueger * @version $Id$ */ public class ShaPasswordEncoderTests extends TestCase { @@ -36,13 +37,36 @@ public class ShaPasswordEncoderTests extends TestCase { String encoded = pe.encodePassword(raw, salt); assertTrue(pe.isPasswordValid(encoded, raw, salt)); assertFalse(pe.isPasswordValid(encoded, badRaw, salt)); - assertTrue(encoded.length() == 40); + assertEquals("b2f50ffcbd3407fe9415c062d55f54731f340d32", encoded); - // now try Base64 + } + + public void testBase64() throws Exception { + ShaPasswordEncoder pe = new ShaPasswordEncoder(); pe.setEncodeHashAsBase64(true); - encoded = pe.encodePassword(raw, salt); + String raw = "abc123"; + String badRaw = "abc321"; + String salt = "THIS_IS_A_SALT"; + String encoded = pe.encodePassword(raw, salt); assertTrue(pe.isPasswordValid(encoded, raw, salt)); assertFalse(pe.isPasswordValid(encoded, badRaw, salt)); assertTrue(encoded.length() != 40); } + + public void test256() throws Exception { + ShaPasswordEncoder pe = new ShaPasswordEncoder(256); + String encoded = pe.encodePassword("abc123", null); + assertEquals("6ca13d52ca70c883e0f0bb101e425a89e8624de51db2d2392593af6a84118090", encoded); + String encodedWithSalt = pe.encodePassword("abc123", "THIS_IS_A_SALT"); + assertEquals("4b79b7de23eb23b78cc5ede227d532b8a51f89b2ec166f808af76b0dbedc47d7", encodedWithSalt); + } + + public void testInvalidStrength() throws Exception { + try { + new ShaPasswordEncoder(666); + fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException e) { + //expected + } + } }