From 96fa2a5a75c2b5a15964781c6873a5eef91ec4df Mon Sep 17 00:00:00 2001 From: Ben Alex Date: Sun, 18 Apr 2004 11:56:50 +0000 Subject: [PATCH] Update encoders so they process salts. --- .../encoding/BaseDigestPasswordEncoder.java | 2 +- .../encoding/BasePasswordEncoder.java | 115 +++++++++++++ .../encoding/Md5PasswordEncoder.java | 9 +- .../encoding/PlaintextPasswordEncoder.java | 41 ++++- .../encoding/ShaPasswordEncoder.java | 9 +- .../providers/encoding/package.html | 5 + .../encoding/BasePasswordEncoderTests.java | 158 ++++++++++++++++++ .../encoding/Md5PasswordEncoderTests.java | 50 ++++++ .../PlaintextPasswordEncoderTests.java | 69 ++++++++ .../encoding/ShaPasswordEncoderTests.java | 50 ++++++ 10 files changed, 499 insertions(+), 9 deletions(-) create mode 100644 core/src/main/java/org/acegisecurity/providers/encoding/BasePasswordEncoder.java create mode 100644 core/src/main/java/org/acegisecurity/providers/encoding/package.html create mode 100644 core/src/test/java/org/acegisecurity/providers/encoding/BasePasswordEncoderTests.java create mode 100644 core/src/test/java/org/acegisecurity/providers/encoding/Md5PasswordEncoderTests.java create mode 100644 core/src/test/java/org/acegisecurity/providers/encoding/PlaintextPasswordEncoderTests.java create mode 100644 core/src/test/java/org/acegisecurity/providers/encoding/ShaPasswordEncoderTests.java diff --git a/core/src/main/java/org/acegisecurity/providers/encoding/BaseDigestPasswordEncoder.java b/core/src/main/java/org/acegisecurity/providers/encoding/BaseDigestPasswordEncoder.java index 461e52eb04..859c2b1e8e 100644 --- a/core/src/main/java/org/acegisecurity/providers/encoding/BaseDigestPasswordEncoder.java +++ b/core/src/main/java/org/acegisecurity/providers/encoding/BaseDigestPasswordEncoder.java @@ -23,7 +23,7 @@ package net.sf.acegisecurity.providers.encoding; * @author colin sampaleanu * @version $Id$ */ -public abstract class BaseDigestPasswordEncoder implements PasswordEncoder { +public abstract class BaseDigestPasswordEncoder extends BasePasswordEncoder { //~ Instance fields ======================================================== private boolean encodeHashAsBase64 = false; diff --git a/core/src/main/java/org/acegisecurity/providers/encoding/BasePasswordEncoder.java b/core/src/main/java/org/acegisecurity/providers/encoding/BasePasswordEncoder.java new file mode 100644 index 0000000000..5f3b74f003 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/encoding/BasePasswordEncoder.java @@ -0,0 +1,115 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.providers.encoding; + +/** + *

+ * Convenience base for all password encoders. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public abstract class BasePasswordEncoder implements PasswordEncoder { + //~ Methods ================================================================ + + /** + * Used by subclasses to extract the password and salt from a merged + * String created using {@link + * #mergePasswordAndSalt(String,Object,boolean)}. + * + *

+ * The first element in the returned array is the password. The second + * element is the salt. The salt array element will always be present, + * even if no salt was found in the mergedPasswordSalt + * argument. + *

+ * + * @param mergedPasswordSalt as generated by + * mergePasswordAndSalt + * + * @return an array, in which the first element is the password and the + * second the salt + * + * @throws IllegalArgumentException DOCUMENT ME! + */ + protected String[] demergePasswordAndSalt(String mergedPasswordSalt) { + if ((mergedPasswordSalt == null) || "".equals(mergedPasswordSalt)) { + throw new IllegalArgumentException( + "Cannot pass a null or empty String"); + } + + String password = mergedPasswordSalt; + String salt = ""; + + int saltBegins = mergedPasswordSalt.lastIndexOf("{"); + + if ((saltBegins != -1) + && ((saltBegins + 1) < mergedPasswordSalt.length())) { + salt = mergedPasswordSalt.substring(saltBegins + 1, + mergedPasswordSalt.length() - 1); + password = mergedPasswordSalt.substring(0, saltBegins); + } + + return new String[] {password, salt}; + } + + /** + * Used by subclasses to generate a merged password and salt + * String. + * + *

+ * The generated password will be in the form of + * password{salt}. + *

+ * + *

+ * A null can be passed to either method, and will be handled + * correctly. If the salt is null or empty, the + * resulting generated password will simply be the passed + * password. The toString method of the + * salt will be used to represent the salt. + *

+ * + * @param password the password to be used (can be null) + * @param salt the salt to be used (can be null) + * @param strict ensures salt doesn't contain the delimiters + * + * @return a merged password and salt String + * + * @throws IllegalArgumentException DOCUMENT ME! + */ + protected String mergePasswordAndSalt(String password, Object salt, + boolean strict) { + if (password == null) { + password = ""; + } + + if (strict && (salt != null)) { + if ((salt.toString().lastIndexOf("{") != -1) + || (salt.toString().lastIndexOf("}") != -1)) { + throw new IllegalArgumentException( + "Cannot use { or } in salt.toString()"); + } + } + + if ((salt == null) || "".equals(salt)) { + return password; + } else { + return password + "{" + salt.toString() + "}"; + } + } +} 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 1b8e67ff85..9502f412ad 100644 --- a/core/src/main/java/org/acegisecurity/providers/encoding/Md5PasswordEncoder.java +++ b/core/src/main/java/org/acegisecurity/providers/encoding/Md5PasswordEncoder.java @@ -28,8 +28,13 @@ import org.apache.commons.codec.digest.DigestUtils; * 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. + *

* * @author colin sampaleanu + * @author Ben Alex * @version $Id$ */ public class Md5PasswordEncoder extends BaseDigestPasswordEncoder @@ -38,13 +43,13 @@ public class Md5PasswordEncoder extends BaseDigestPasswordEncoder public boolean isPasswordValid(String encPass, String rawPass, Object salt) { String pass1 = "" + encPass; - String pass2 = encodeInternal("" + rawPass); + String pass2 = encodeInternal(mergePasswordAndSalt(rawPass, salt, false)); return pass1.equals(pass2); } public String encodePassword(String rawPass, Object salt) { - return encodeInternal("" + rawPass); + return encodeInternal(mergePasswordAndSalt(rawPass, salt, false)); } private String encodeInternal(String input) { diff --git a/core/src/main/java/org/acegisecurity/providers/encoding/PlaintextPasswordEncoder.java b/core/src/main/java/org/acegisecurity/providers/encoding/PlaintextPasswordEncoder.java index 6aefc663bd..a9cec42a45 100644 --- a/core/src/main/java/org/acegisecurity/providers/encoding/PlaintextPasswordEncoder.java +++ b/core/src/main/java/org/acegisecurity/providers/encoding/PlaintextPasswordEncoder.java @@ -19,11 +19,18 @@ package net.sf.acegisecurity.providers.encoding; *

* Plaintext implementation of PasswordEncoder. *

+ * + *

+ * As callers may wish to extract the password and salts separately from the + * encoded password, the salt must not contain reserved characters + * (specifically '{' and '}'). + *

* * @author colin sampaleanu + * @author Ben Alex * @version $Id$ */ -public class PlaintextPasswordEncoder implements PasswordEncoder { +public class PlaintextPasswordEncoder extends BasePasswordEncoder { //~ Instance fields ======================================================== private boolean ignorePasswordCase = false; @@ -49,8 +56,12 @@ public class PlaintextPasswordEncoder implements PasswordEncoder { } public boolean isPasswordValid(String encPass, String rawPass, Object salt) { - String pass1 = "" + encPass; - String pass2 = "" + rawPass; + String pass1 = encPass + ""; + + // Strict delimiters is false because pass2 never persisted anywhere + // and we want to avoid unnecessary exceptions as a result (the + // authentication will fail as the encodePassword never allows them) + String pass2 = mergePasswordAndSalt(rawPass, salt, false); if (!ignorePasswordCase) { return pass1.equals(pass2); @@ -60,6 +71,28 @@ public class PlaintextPasswordEncoder implements PasswordEncoder { } public String encodePassword(String rawPass, Object salt) { - return rawPass; + return mergePasswordAndSalt(rawPass, salt, true); + } + + /** + * Demerges the previously {@link #encodePassword(String, + * Object)}String. + * + *

+ * The resulting array is guaranteed to always contain two elements. The + * first is the password, and the second is the salt. + *

+ * + *

+ * Throws an exception if null or an empty String + * is passed to the method. + *

+ * + * @param password from {@link #encodePassword(String, Object)} + * + * @return an array containing the password and salt + */ + public String[] obtainPasswordAndSalt(String password) { + return demergePasswordAndSalt(password); } } 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 307371fb29..913751705e 100644 --- a/core/src/main/java/org/acegisecurity/providers/encoding/ShaPasswordEncoder.java +++ b/core/src/main/java/org/acegisecurity/providers/encoding/ShaPasswordEncoder.java @@ -28,8 +28,13 @@ import org.apache.commons.codec.digest.DigestUtils; * 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. + *

* * @author colin sampaleanu + * @author Ben Alex * @version $Id$ */ public class ShaPasswordEncoder extends BaseDigestPasswordEncoder @@ -38,13 +43,13 @@ public class ShaPasswordEncoder extends BaseDigestPasswordEncoder public boolean isPasswordValid(String encPass, String rawPass, Object salt) { String pass1 = "" + encPass; - String pass2 = encodeInternal("" + rawPass); + String pass2 = encodeInternal(mergePasswordAndSalt(rawPass, salt, false)); return pass1.equals(pass2); } public String encodePassword(String rawPass, Object salt) { - return encodeInternal("" + rawPass); + return encodeInternal(mergePasswordAndSalt(rawPass, salt, false)); } private String encodeInternal(String input) { diff --git a/core/src/main/java/org/acegisecurity/providers/encoding/package.html b/core/src/main/java/org/acegisecurity/providers/encoding/package.html new file mode 100644 index 0000000000..c6b8499dbb --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/encoding/package.html @@ -0,0 +1,5 @@ + + +Password encoding implementations. + + diff --git a/core/src/test/java/org/acegisecurity/providers/encoding/BasePasswordEncoderTests.java b/core/src/test/java/org/acegisecurity/providers/encoding/BasePasswordEncoderTests.java new file mode 100644 index 0000000000..735b7d2ed0 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/encoding/BasePasswordEncoderTests.java @@ -0,0 +1,158 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.providers.encoding; + +import junit.framework.TestCase; + +import org.springframework.dao.DataAccessException; + + +/** + *

+ * TestCase for BasePasswordEncoder. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class BasePasswordEncoderTests extends TestCase { + //~ Methods ================================================================ + + public void testDemergeHandlesEmptyAndNullSalts() { + MockPasswordEncoder pwd = new MockPasswordEncoder(); + + String merged = pwd.nowMergePasswordAndSalt("password", null, true); + + String[] demerged = pwd.nowDemergePasswordAndSalt(merged); + assertEquals("password", demerged[0]); + assertEquals("", demerged[1]); + + merged = pwd.nowMergePasswordAndSalt("password", "", true); + + demerged = pwd.nowDemergePasswordAndSalt(merged); + assertEquals("password", demerged[0]); + assertEquals("", demerged[1]); + } + + public void testDemergeWithEmptyStringIsRejected() { + MockPasswordEncoder pwd = new MockPasswordEncoder(); + + try { + pwd.nowDemergePasswordAndSalt(""); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("Cannot pass a null or empty String", + expected.getMessage()); + } + } + + public void testDemergeWithNullIsRejected() { + MockPasswordEncoder pwd = new MockPasswordEncoder(); + + try { + pwd.nowDemergePasswordAndSalt(null); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("Cannot pass a null or empty String", + expected.getMessage()); + } + } + + public void testMergeDemerge() { + MockPasswordEncoder pwd = new MockPasswordEncoder(); + + String merged = pwd.nowMergePasswordAndSalt("password", "foo", true); + assertEquals("password{foo}", merged); + + String[] demerged = pwd.nowDemergePasswordAndSalt(merged); + assertEquals("password", demerged[0]); + assertEquals("foo", demerged[1]); + } + + public void testMergeDemergeWithDelimitersInPassword() { + MockPasswordEncoder pwd = new MockPasswordEncoder(); + + String merged = pwd.nowMergePasswordAndSalt("p{ass{w{o}rd", "foo", true); + assertEquals("p{ass{w{o}rd{foo}", merged); + + String[] demerged = pwd.nowDemergePasswordAndSalt(merged); + System.out.println(demerged[0]); + System.out.println(demerged[1]); + + assertEquals("p{ass{w{o}rd", demerged[0]); + assertEquals("foo", demerged[1]); + } + + public void testMergeDemergeWithNullAsPassword() { + MockPasswordEncoder pwd = new MockPasswordEncoder(); + + String merged = pwd.nowMergePasswordAndSalt(null, "foo", true); + assertEquals("{foo}", merged); + + String[] demerged = pwd.nowDemergePasswordAndSalt(merged); + assertEquals("", demerged[0]); + assertEquals("foo", demerged[1]); + } + + public void testStrictMergeRejectsDelimitersInSalt1() { + MockPasswordEncoder pwd = new MockPasswordEncoder(); + + try { + pwd.nowMergePasswordAndSalt("password", "f{oo", true); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("Cannot use { or } in salt.toString()", + expected.getMessage()); + } + } + + public void testStrictMergeRejectsDelimitersInSalt2() { + MockPasswordEncoder pwd = new MockPasswordEncoder(); + + try { + pwd.nowMergePasswordAndSalt("password", "f}oo", true); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("Cannot use { or } in salt.toString()", + expected.getMessage()); + } + } + + //~ Inner Classes ========================================================== + + private class MockPasswordEncoder extends BasePasswordEncoder { + public boolean isPasswordValid(String encPass, String rawPass, + Object salt) throws DataAccessException { + throw new UnsupportedOperationException( + "mock method not implemented"); + } + + public String encodePassword(String rawPass, Object salt) + throws DataAccessException { + throw new UnsupportedOperationException( + "mock method not implemented"); + } + + public String[] nowDemergePasswordAndSalt(String password) { + return demergePasswordAndSalt(password); + } + + public String nowMergePasswordAndSalt(String password, Object salt, + boolean strict) { + return mergePasswordAndSalt(password, salt, strict); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/encoding/Md5PasswordEncoderTests.java b/core/src/test/java/org/acegisecurity/providers/encoding/Md5PasswordEncoderTests.java new file mode 100644 index 0000000000..cd1f02786b --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/encoding/Md5PasswordEncoderTests.java @@ -0,0 +1,50 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.providers.encoding; + +import junit.framework.TestCase; + + +/** + *

+ * TestCase for PlaintextPasswordEncoder. + *

+ * + * @author colin sampaleanu + * @author Ben Alex + * @version $Id$ + */ +public class Md5PasswordEncoderTests extends TestCase { + //~ Methods ================================================================ + + public void testBasicFunctionality() { + Md5PasswordEncoder pe = new Md5PasswordEncoder(); + 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); + + // now try Base64 + pe.setEncodeHashAsBase64(true); + 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/PlaintextPasswordEncoderTests.java b/core/src/test/java/org/acegisecurity/providers/encoding/PlaintextPasswordEncoderTests.java new file mode 100644 index 0000000000..e7d919551d --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/encoding/PlaintextPasswordEncoderTests.java @@ -0,0 +1,69 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.providers.encoding; + +import junit.framework.TestCase; + + +/** + *

+ * TestCase for PlaintextPasswordEncoder. + *

+ * + * @author colin sampaleanu + * @author Ben Alex + * @version $Id$ + */ +public class PlaintextPasswordEncoderTests extends TestCase { + //~ Methods ================================================================ + + public void testBasicFunctionality() { + PlaintextPasswordEncoder pe = new PlaintextPasswordEncoder(); + + String raw = "abc123"; + String rawDiffCase = "AbC123"; + String badRaw = "abc321"; + String salt = "THIS_IS_A_SALT"; + + String encoded = pe.encodePassword(raw, salt); + assertEquals("abc123{THIS_IS_A_SALT}", encoded); + assertTrue(pe.isPasswordValid(encoded, raw, salt)); + assertFalse(pe.isPasswordValid(encoded, badRaw, salt)); + + // make sure default is not to ignore password case + assertFalse(pe.isIgnorePasswordCase()); + encoded = pe.encodePassword(rawDiffCase, salt); + assertFalse(pe.isPasswordValid(encoded, raw, salt)); + + // now check for ignore password case + pe = new PlaintextPasswordEncoder(); + pe.setIgnorePasswordCase(true); + + // should be able to validate even without encoding + encoded = pe.encodePassword(rawDiffCase, salt); + assertTrue(pe.isPasswordValid(encoded, raw, salt)); + assertFalse(pe.isPasswordValid(encoded, badRaw, salt)); + } + + public void testMergeDemerge() { + PlaintextPasswordEncoder pwd = new PlaintextPasswordEncoder(); + + String merged = pwd.encodePassword("password", "foo"); + String[] demerged = pwd.obtainPasswordAndSalt(merged); + assertEquals("password", demerged[0]); + assertEquals("foo", demerged[1]); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/encoding/ShaPasswordEncoderTests.java b/core/src/test/java/org/acegisecurity/providers/encoding/ShaPasswordEncoderTests.java new file mode 100644 index 0000000000..20531cebff --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/encoding/ShaPasswordEncoderTests.java @@ -0,0 +1,50 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.providers.encoding; + +import junit.framework.TestCase; + + +/** + *

+ * TestCase for ShaPasswordEncoder. + *

+ * + * @author colin sampaleanu + * @author Ben Alex + * @version $Id$ + */ +public class ShaPasswordEncoderTests extends TestCase { + //~ Methods ================================================================ + + public void testBasicFunctionality() { + ShaPasswordEncoder pe = new ShaPasswordEncoder(); + 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); + + // now try Base64 + pe.setEncodeHashAsBase64(true); + encoded = pe.encodePassword(raw, salt); + assertTrue(pe.isPasswordValid(encoded, raw, salt)); + assertFalse(pe.isPasswordValid(encoded, badRaw, salt)); + assertTrue(encoded.length() != 40); + } +}