From 0f612bf637c16251dff148ceef65c188f5e726a5 Mon Sep 17 00:00:00 2001
From: Rob Winch
+ * String idForEncode = "bcrypt"; + * Map+ * + * + *encoders = new HashMap<>(); + * encoders.put(idForEncode, new BCryptPasswordEncoder()); + * encoders.put("noop", NoOpPasswordEncoder.getInstance()); + * encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); + * encoders.put("scrypt", new SCryptPasswordEncoder()); + * encoders.put("sha256", new StandardPasswordEncoder()); + * + * PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders); + *
+ * {id}encodedPassword
+ *
+ *
+ * Such that "id" is an identifier used to look up which {@link PasswordEncoder} should
+ * be used and "encodedPassword" is the original encoded password for the selected
+ * {@link PasswordEncoder}. The "id" must be at the beginning of the password, start with
+ * "{" and end with "}". If the "id" cannot be found, the "id" will be null.
+ *
+ * For example, the following might be a list of passwords encoded using different "id".
+ * All of the original passwords are "password".
+ *
+ *
+ * {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
+ * {noop}password
+ * {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
+ * {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
+ * {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
+ *
+ *
+ * For the DelegatingPasswordEncoder that we constructed above:
+ *
+ *
+ * {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
+ *
+ *
+ * + The encodedPassword provided will be the full password + * passed in including the {"id"} portion.* For example, if the password of + * "{notmapped}foobar" was used, the "id" would be "notmapped" and the encodedPassword + * passed into the {@link PasswordEncoder} would be "{notmapped}foobar". + *
+ * @param defaultPasswordEncoderForMatches the encoder to use. The default is to + * throw an {@link IllegalArgumentException} + */ + public void setDefaultPasswordEncoderForMatches( + PasswordEncoder defaultPasswordEncoderForMatches) { + if(defaultPasswordEncoderForMatches == null) { + throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null"); + } + this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches; + } + + @Override + public String encode(CharSequence rawPassword) { + return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword); + } + + @Override + public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { + if(rawPassword == null && prefixEncodedPassword == null) { + return true; + } + String id = extractId(prefixEncodedPassword); + PasswordEncoder delegate = this.idToPasswordEncoder.get(id); + if(delegate == null) { + return this.defaultPasswordEncoderForMatches + .matches(rawPassword, prefixEncodedPassword); + } + String encodedPassword = extractEncodedPassword(prefixEncodedPassword); + return delegate.matches(rawPassword, encodedPassword); + } + + private String extractId(String prefixEncodedPassword) { + if (prefixEncodedPassword == null) { + return null; + } + int start = prefixEncodedPassword.indexOf(PREFIX); + if(start != 0) { + return null; + } + int end = prefixEncodedPassword.indexOf(SUFFIX, start); + if(end < 0) { + return null; + } + return prefixEncodedPassword.substring(start + 1, end); + } + + private String extractEncodedPassword(String prefixEncodedPassword) { + int start = prefixEncodedPassword.indexOf(SUFFIX); + return prefixEncodedPassword.substring(start + 1); + } + + /** + * Default {@link PasswordEncoder} that throws an exception that a id could + */ + private class UnmappedIdPasswordEncoder implements PasswordEncoder { + + @Override + public String encode(CharSequence rawPassword) { + throw new UnsupportedOperationException("encode is not supported"); + } + + @Override + public boolean matches(CharSequence rawPassword, + String prefixEncodedPassword) { + String id = extractId(prefixEncodedPassword); + throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\""); + } + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Digester.java b/crypto/src/main/java/org/springframework/security/crypto/password/Digester.java index d42ec3a821..35e9b600de 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/Digester.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Digester.java @@ -31,7 +31,7 @@ final class Digester { private final String algorithm; - private final int iterations; + private int iterations; /** * Create a new Digester. @@ -42,7 +42,7 @@ final class Digester { // eagerly validate the algorithm createDigest(algorithm); this.algorithm = algorithm; - this.iterations = iterations; + setIterations(iterations); } public byte[] digest(byte[] value) { @@ -53,6 +53,13 @@ final class Digester { return value; } + final void setIterations(int iterations) { + if(iterations <= 0) { + throw new IllegalArgumentException("Iterations value must be greater than zero"); + } + this.iterations = iterations; + } + private static MessageDigest createDigest(String algorithm) { try { return MessageDigest.getInstance(algorithm); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java new file mode 100644 index 0000000000..b7bc73bb0e --- /dev/null +++ b/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * 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 org.springframework.security.crypto.password; + +import org.springframework.security.crypto.codec.Utf8; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; + +import java.security.MessageDigest; +import java.util.Base64; + +/** + * This {@link PasswordEncoder} is provided for legacy purposes only and is not considered + * secure. + * + * A version of {@link PasswordEncoder} which supports Ldap SHA and SSHA (salted-SHA) + * encodings. The values are base-64 encoded and have the label "{SHA}" (or "{SSHA}") + * prepended to the encoded hash. These can be made lower-case in the encoded password, if + * required, by setting the forceLowerCasePrefix property to true. + * + * Also supports plain text passwords, so can safely be used in cases when both encoded + * and non-encoded passwords are in use or when a null implementation is required. + * + * @author Luke Taylor + * @since 4.2.6 + * @deprecated Digest based password encoding is not considered secure. Instead use an + * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or + * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports + * password upgrades. There are no plans to remove this support. It is deprecated to indicate + * that this is a legacy implementation and using it is considered insecure. + */ +@Deprecated +public class LdapShaPasswordEncoder implements PasswordEncoder { + // ~ Static fields/initializers + // ===================================================================================== + + /** The number of bytes in a SHA hash */ + private static final int SHA_LENGTH = 20; + private static final String SSHA_PREFIX = "{SSHA}"; + private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase(); + private static final String SHA_PREFIX = "{SHA}"; + private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase(); + + // ~ Instance fields + // ================================================================================================ + private BytesKeyGenerator saltGenerator; + + private boolean forceLowerCasePrefix; + + // ~ Constructors + // =================================================================================================== + + public LdapShaPasswordEncoder() { + this(KeyGenerators.secureRandom()); + } + + public LdapShaPasswordEncoder(BytesKeyGenerator saltGenerator) { + if(saltGenerator == null) { + throw new IllegalArgumentException("saltGenerator cannot be null"); + } + this.saltGenerator = saltGenerator; + } + + // ~ Methods + // ======================================================================================================== + + private byte[] combineHashAndSalt(byte[] hash, byte[] salt) { + if (salt == null) { + return hash; + } + + byte[] hashAndSalt = new byte[hash.length + salt.length]; + System.arraycopy(hash, 0, hashAndSalt, 0, hash.length); + System.arraycopy(salt, 0, hashAndSalt, hash.length, salt.length); + + return hashAndSalt; + } + + /** + * Calculates the hash of password (and salt bytes, if supplied) and returns a base64 + * encoded concatenation of the hash and salt, prefixed with {SHA} (or {SSHA} if salt + * was used). + * + * @param rawPass the password to be encoded. + * + * @return the encoded password in the specified format + * + */ + public String encode(CharSequence rawPass) { + byte[] salt = this.saltGenerator.generateKey(); + return encode(rawPass, salt); + } + + + private String encode(CharSequence rawPassword, byte[] salt) { + MessageDigest sha; + + try { + sha = MessageDigest.getInstance("SHA"); + sha.update(Utf8.encode(rawPassword)); + } + catch (java.security.NoSuchAlgorithmException e) { + throw new IllegalStateException("No SHA implementation available!"); + } + + if (salt != null) { + sha.update(salt); + } + + byte[] hash = combineHashAndSalt(sha.digest(), (byte[]) salt); + + String prefix; + + if (salt == null || salt.length == 0) { + prefix = forceLowerCasePrefix ? SHA_PREFIX_LC : SHA_PREFIX; + } + else { + prefix = forceLowerCasePrefix ? SSHA_PREFIX_LC : SSHA_PREFIX; + } + + return prefix + Utf8.decode(Base64.getEncoder().encode(hash)); + } + + private byte[] extractSalt(String encPass) { + String encPassNoLabel = encPass.substring(6); + + byte[] hashAndSalt = Base64.getDecoder().decode(encPassNoLabel.getBytes()); + int saltLength = hashAndSalt.length - SHA_LENGTH; + byte[] salt = new byte[saltLength]; + System.arraycopy(hashAndSalt, SHA_LENGTH, salt, 0, saltLength); + + return salt; + } + + /** + * Checks the validity of an unencoded password against an encoded one in the form + * "{SSHA}sQuQF8vj8Eg2Y1hPdh3bkQhCKQBgjhQI". + * + * @param rawPassword unencoded password to be verified. + * @param encodedPassword the actual SSHA or SHA encoded password + * + * @return true if they match (independent of the case of the prefix). + */ + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return matches(rawPassword == null ? null : rawPassword.toString(), encodedPassword); + } + + private boolean matches(String rawPassword, String encodedPassword) { + String prefix = extractPrefix(encodedPassword); + + if (prefix == null) { + return PasswordEncoderUtils.equals(encodedPassword, rawPassword); + } + + byte[] salt; + if (prefix.equals(SSHA_PREFIX) || prefix.equals(SSHA_PREFIX_LC)) { + salt = extractSalt(encodedPassword); + } + else if (!prefix.equals(SHA_PREFIX) && !prefix.equals(SHA_PREFIX_LC)) { + throw new IllegalArgumentException("Unsupported password prefix '" + prefix + + "'"); + } + else { + // Standard SHA + salt = null; + } + + int startOfHash = prefix.length(); + + String encodedRawPass = encode(rawPassword, salt).substring(startOfHash); + + return PasswordEncoderUtils + .equals(encodedRawPass, encodedPassword.substring(startOfHash)); + } + + /** + * Returns the hash prefix or null if there isn't one. + */ + private String extractPrefix(String encPass) { + if (!encPass.startsWith("{")) { + return null; + } + + int secondBrace = encPass.lastIndexOf('}'); + + if (secondBrace < 0) { + throw new IllegalArgumentException( + "Couldn't find closing brace for SHA prefix"); + } + + return encPass.substring(0, secondBrace + 1); + } + + public void setForceLowerCasePrefix(boolean forceLowerCasePrefix) { + this.forceLowerCasePrefix = forceLowerCasePrefix; + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Md4.java b/crypto/src/main/java/org/springframework/security/crypto/password/Md4.java new file mode 100644 index 0000000000..4bea3dab3c --- /dev/null +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Md4.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * 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 org.springframework.security.crypto.password; + +/** + * Implementation of the MD4 message digest derived from the RSA Data Security, Inc, MD4 + * Message-Digest Algorithm. + * + * @author Alan Stewart + */ +class Md4 { + private static final int BLOCK_SIZE = 64; + private static final int HASH_SIZE = 16; + private final byte[] buffer = new byte[BLOCK_SIZE]; + private int bufferOffset; + private long byteCount; + private final int[] state = new int[4]; + private final int[] tmp = new int[16]; + + Md4() { + reset(); + } + + public void reset() { + bufferOffset = 0; + byteCount = 0; + state[0] = 0x67452301; + state[1] = 0xEFCDAB89; + state[2] = 0x98BADCFE; + state[3] = 0x10325476; + } + + public byte[] digest() { + byte[] resBuf = new byte[HASH_SIZE]; + digest(resBuf, 0, HASH_SIZE); + return resBuf; + } + + private void digest(byte[] buffer, int off) { + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + buffer[off + (i * 4 + j)] = (byte) (state[i] >>> (8 * j)); + } + } + } + + private void digest(byte[] buffer, int offset, int len) { + this.buffer[this.bufferOffset++] = (byte) 0x80; + int lenOfBitLen = 8; + int C = BLOCK_SIZE - lenOfBitLen; + if (this.bufferOffset > C) { + while (this.bufferOffset < BLOCK_SIZE) { + this.buffer[this.bufferOffset++] = (byte) 0x00; + } + update(this.buffer, 0); + this.bufferOffset = 0; + } + + while (this.bufferOffset < C) { + this.buffer[this.bufferOffset++] = (byte) 0x00; + } + + long bitCount = byteCount * 8; + for (int i = 0; i < 64; i += 8) { + this.buffer[this.bufferOffset++] = (byte) (bitCount >>> (i)); + } + + update(this.buffer, 0); + digest(buffer, offset); + } + + public void update(byte[] input, int offset, int length) { + byteCount += length; + int todo; + while (length >= (todo = BLOCK_SIZE - this.bufferOffset)) { + System.arraycopy(input, offset, this.buffer, this.bufferOffset, todo); + update(this.buffer, 0); + length -= todo; + offset += todo; + this.bufferOffset = 0; + } + + System.arraycopy(input, offset, this.buffer, this.bufferOffset, length); + bufferOffset += length; + } + + private void update(byte[] block, int offset) { + for (int i = 0; i < 16; i++) { + tmp[i] = (block[offset++] & 0xFF) | (block[offset++] & 0xFF) << 8 + | (block[offset++] & 0xFF) << 16 | (block[offset++] & 0xFF) << 24; + } + + int A = state[0]; + int B = state[1]; + int C = state[2]; + int D = state[3]; + + A = FF(A, B, C, D, tmp[0], 3); + D = FF(D, A, B, C, tmp[1], 7); + C = FF(C, D, A, B, tmp[2], 11); + B = FF(B, C, D, A, tmp[3], 19); + A = FF(A, B, C, D, tmp[4], 3); + D = FF(D, A, B, C, tmp[5], 7); + C = FF(C, D, A, B, tmp[6], 11); + B = FF(B, C, D, A, tmp[7], 19); + A = FF(A, B, C, D, tmp[8], 3); + D = FF(D, A, B, C, tmp[9], 7); + C = FF(C, D, A, B, tmp[10], 11); + B = FF(B, C, D, A, tmp[11], 19); + A = FF(A, B, C, D, tmp[12], 3); + D = FF(D, A, B, C, tmp[13], 7); + C = FF(C, D, A, B, tmp[14], 11); + B = FF(B, C, D, A, tmp[15], 19); + + A = GG(A, B, C, D, tmp[0], 3); + D = GG(D, A, B, C, tmp[4], 5); + C = GG(C, D, A, B, tmp[8], 9); + B = GG(B, C, D, A, tmp[12], 13); + A = GG(A, B, C, D, tmp[1], 3); + D = GG(D, A, B, C, tmp[5], 5); + C = GG(C, D, A, B, tmp[9], 9); + B = GG(B, C, D, A, tmp[13], 13); + A = GG(A, B, C, D, tmp[2], 3); + D = GG(D, A, B, C, tmp[6], 5); + C = GG(C, D, A, B, tmp[10], 9); + B = GG(B, C, D, A, tmp[14], 13); + A = GG(A, B, C, D, tmp[3], 3); + D = GG(D, A, B, C, tmp[7], 5); + C = GG(C, D, A, B, tmp[11], 9); + B = GG(B, C, D, A, tmp[15], 13); + + A = HH(A, B, C, D, tmp[0], 3); + D = HH(D, A, B, C, tmp[8], 9); + C = HH(C, D, A, B, tmp[4], 11); + B = HH(B, C, D, A, tmp[12], 15); + A = HH(A, B, C, D, tmp[2], 3); + D = HH(D, A, B, C, tmp[10], 9); + C = HH(C, D, A, B, tmp[6], 11); + B = HH(B, C, D, A, tmp[14], 15); + A = HH(A, B, C, D, tmp[1], 3); + D = HH(D, A, B, C, tmp[9], 9); + C = HH(C, D, A, B, tmp[5], 11); + B = HH(B, C, D, A, tmp[13], 15); + A = HH(A, B, C, D, tmp[3], 3); + D = HH(D, A, B, C, tmp[11], 9); + C = HH(C, D, A, B, tmp[7], 11); + B = HH(B, C, D, A, tmp[15], 15); + + state[0] += A; + state[1] += B; + state[2] += C; + state[3] += D; + } + + private int FF(int a, int b, int c, int d, int x, int s) { + int t = a + ((b & c) | (~b & d)) + x; + return t << s | t >>> (32 - s); + } + + private int GG(int a, int b, int c, int d, int x, int s) { + int t = a + ((b & (c | d)) | (c & d)) + x + 0x5A827999; + return t << s | t >>> (32 - s); + } + + private int HH(int a, int b, int c, int d, int x, int s) { + int t = a + (b ^ c ^ d) + x + 0x6ED9EBA1; + return t << s | t >>> (32 - s); + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java new file mode 100644 index 0000000000..33d64a8ec0 --- /dev/null +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * 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 org.springframework.security.crypto.password; + +import org.springframework.security.crypto.codec.Hex; +import org.springframework.security.crypto.codec.Utf8; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; + +import java.util.Base64; + +/** + * This {@link PasswordEncoder} is provided for legacy purposes only and is not considered secure. + * + * Encodes passwords using MD4. The general format of the password is: + * + *
+ * s = salt == null ? "" : "{" + salt + "}"
+ * s + md4(password + s)
+ *
+ *
+ * Such that "salt" is the salt, md4 is the digest method, and password is the actual
+ * password. For example with a password of "password", and a salt of
+ * "thisissalt":
+ *
+ *
+ * String s = salt == null ? "" : "{" + salt + "}";
+ * s + md4(password + s)
+ * "{thisissalt}" + md4(password + "{thisissalt}")
+ * "{thisissalt}6cc7924dad12ade79dfb99e424f25260"
+ *
+ *
+ * If the salt does not exist, then omit "{salt}" like this:
+ *
+ * + * md4(password) + *+ * + * If the salt is an empty String, then only use "{}" like this: + * + *
+ * "{}" + md4(password + "{}")
+ *
+ *
+ * The format is intended to work with the Md4PasswordEncoder that was found in the
+ * Spring Security core module. However, the passwords will need to be migrated to include
+ * any salt with the password since this API provides Salt internally vs making it the
+ * responsibility of the user. To migrate passwords from the SaltSource use the following:
+ *
+ *
+ * String salt = saltSource.getSalt(user);
+ * String s = salt == null ? null : "{" + salt + "}";
+ * String migratedPassword = s + user.getPassword();
+ *
+ *
+ * @author Ray Krueger
+ * @author Luke Taylor
+ * @author Rob winch
+ * @since 5.1
+ * @deprecated Digest based password encoding is not considered secure. Instead use an
+ * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
+ * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
+ * password upgrades. There are no plans to remove this support. It is deprecated to indicate
+ * that this is a legacy implementation and using it is considered insecure.
+ */
+@Deprecated
+public class Md4PasswordEncoder implements PasswordEncoder {
+ private static final String PREFIX = "{";
+ private static final String SUFFIX = "}";
+ private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator();
+ private boolean encodeHashAsBase64;
+
+ private Digester digester;
+
+
+ public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
+ this.encodeHashAsBase64 = encodeHashAsBase64;
+ }
+
+ /**
+ * Encodes the rawPass using a MessageDigest. If a salt is specified it will be merged
+ * with the password before encoding.
+ *
+ * @param rawPassword The plain text password
+ * @return Hex string of password digest (or base64 encoded string if
+ * encodeHashAsBase64 is enabled.
+ */
+ public String encode(CharSequence rawPassword) {
+ String salt = PREFIX + this.saltGenerator.generateKey() + SUFFIX;
+ return digest(salt, rawPassword);
+ }
+
+ private String digest(String salt, CharSequence rawPassword) {
+ if(rawPassword == null) {
+ rawPassword = "";
+ }
+ String saltedPassword = rawPassword + salt;
+ byte[] saltedPasswordBytes = Utf8.encode(saltedPassword);
+
+ Md4 md4 = new Md4();
+ md4.update(saltedPasswordBytes, 0, saltedPasswordBytes.length);
+
+ byte[] digest = md4.digest();
+ String encoded = encode(digest);
+ return salt + encoded;
+ }
+
+ private String encode(byte[] digest) {
+ if (this.encodeHashAsBase64) {
+ return Utf8.decode(Base64.getEncoder().encode(digest));
+ }
+ else {
+ return new String(Hex.encode(digest));
+ }
+ }
+
+ /**
+ * Takes a previously encoded password and compares it with a rawpassword after mixing
+ * in the salt and encoding that value
+ *
+ * @param rawPassword plain text password
+ * @param encodedPassword previously encoded password
+ * @return true or false
+ */
+ public boolean matches(CharSequence rawPassword, String encodedPassword) {
+ String salt = extractSalt(encodedPassword);
+ String rawPasswordEncoded = digest(salt, rawPassword);
+ return PasswordEncoderUtils.equals(encodedPassword.toString(), rawPasswordEncoded);
+ }
+
+ private String extractSalt(String prefixEncodedPassword) {
+ int start = prefixEncodedPassword.indexOf(PREFIX);
+ if(start != 0) {
+ return "";
+ }
+ int end = prefixEncodedPassword.indexOf(SUFFIX, start);
+ if(end < 0) {
+ return "";
+ }
+ return prefixEncodedPassword.substring(start, end + 1);
+ }
+}
diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java
new file mode 100644
index 0000000000..eb746076f4
--- /dev/null
+++ b/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * 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 org.springframework.security.crypto.password;
+
+import org.springframework.security.crypto.codec.Hex;
+import org.springframework.security.crypto.codec.Utf8;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+
+import java.security.MessageDigest;
+import java.util.Base64;
+
+/**
+ * This {@link PasswordEncoder} is provided for legacy purposes only and is not considered secure.
+ *
+ * Encodes passwords using the passed in {@link MessageDigest}.
+ *
+ * The general format of the password is:
+ *
+ *
+ * s = salt == null ? "" : "{" + salt + "}"
+ * s + digest(password + s)
+ *
+ *
+ * Such that "salt" is the salt, digest is the digest method, and password is the actual
+ * password. For example when using MD5, a password of "password", and a salt of
+ * "thisissalt":
+ *
+ *
+ * String s = salt == null ? "" : "{" + salt + "}";
+ * s + md5(password + s)
+ * "{thisissalt}" + md5(password + "{thisissalt}")
+ * "{thisissalt}2a4e7104c2780098f50ed5a84bb2323d"
+ *
+ *
+ * If the salt does not exist, then omit "{salt}" like this:
+ *
+ * + * digest(password) + *+ * + * If the salt is an empty String, then only use "{}" like this: + * + *
+ * "{}" + digest(password + "{}")
+ *
+ *
+ * The format is intended to work with the DigestPasswordEncoder that was found in the
+ * Spring Security core module. However, the passwords will need to be migrated to include
+ * any salt with the password since this API provides Salt internally vs making it the
+ * responsibility of the user. To migrate passwords from the SaltSource use the following:
+ *
+ *
+ * String salt = saltSource.getSalt(user);
+ * String s = salt == null ? null : "{" + salt + "}";
+ * String migratedPassword = s + user.getPassword();
+ *
+ *
+ * @author Ray Krueger
+ * @author Luke Taylor
+ * @author Rob Winch
+ * @since 4.2.6
+ * @deprecated Digest based password encoding is not considered secure. Instead use an
+ * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
+ * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
+ * password upgrades. There are no plans to remove this support. It is deprecated to indicate
+ * that this is a legacy implementation and using it is considered insecure.
+ */
+@Deprecated
+public class MessageDigestPasswordEncoder implements PasswordEncoder {
+ private static final String PREFIX = "{";
+ private static final String SUFFIX = "}";
+ private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator();
+ private boolean encodeHashAsBase64;
+
+ private Digester digester;
+
+ /**
+ * The digest algorithm to use Supports the named
+ *
+ * Message Digest Algorithms in the Java environment.
+ *
+ * @param algorithm
+ */
+ public MessageDigestPasswordEncoder(String algorithm) {
+ this.digester = new Digester(algorithm, 1);
+ }
+
+ public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
+ this.encodeHashAsBase64 = encodeHashAsBase64;
+ }
+
+ /**
+ * Encodes the rawPass using a MessageDigest. If a salt is specified it will be merged
+ * with the password before encoding.
+ *
+ * @param rawPassword The plain text password
+ * @return Hex string of password digest (or base64 encoded string if
+ * encodeHashAsBase64 is enabled.
+ */
+ public String encode(CharSequence rawPassword) {
+ String salt = PREFIX + this.saltGenerator.generateKey() + SUFFIX;
+ return digest(salt, rawPassword);
+ }
+
+ private String digest(String salt, CharSequence rawPassword) {
+ String saltedPassword = rawPassword + salt;
+
+ byte[] digest = this.digester.digest(Utf8.encode(saltedPassword));
+ String encoded = encode(digest);
+ return salt + encoded;
+ }
+
+ private String encode(byte[] digest) {
+ if (this.encodeHashAsBase64) {
+ return Utf8.decode(Base64.getEncoder().encode(digest));
+ }
+ else {
+ return new String(Hex.encode(digest));
+ }
+ }
+
+ /**
+ * Takes a previously encoded password and compares it with a rawpassword after mixing
+ * in the salt and encoding that value
+ *
+ * @param rawPassword plain text password
+ * @param encodedPassword previously encoded password
+ * @return true or false
+ */
+ public boolean matches(CharSequence rawPassword, String encodedPassword) {
+ String salt = extractSalt(encodedPassword);
+ String rawPasswordEncoded = digest(salt, rawPassword);
+ return PasswordEncoderUtils.equals(encodedPassword.toString(), rawPasswordEncoded);
+ }
+
+ /**
+ * 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) {
+ this.digester.setIterations(iterations);
+ }
+
+ private String extractSalt(String prefixEncodedPassword) {
+ int start = prefixEncodedPassword.indexOf(PREFIX);
+ if(start != 0) {
+ return "";
+ }
+ int end = prefixEncodedPassword.indexOf(SUFFIX, start);
+ if(end < 0) {
+ return "";
+ }
+ return prefixEncodedPassword.substring(start, end + 1);
+ }
+}
diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java
index 9de708bd33..696a89b4b8 100644
--- a/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java
+++ b/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java
@@ -16,11 +16,19 @@
package org.springframework.security.crypto.password;
/**
+ * This {@link PasswordEncoder} is provided for legacy and testing purposes only and is
+ * not considered secure.
+ *
* A password encoder that does nothing. Useful for testing where working with plain text
* passwords may be preferred.
*
* @author Keith Donald
+ * @deprecated This PasswordEncoder is not secure. Instead use an
+ * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
+ * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
+ * password upgrades.
*/
+@Deprecated
public final class NoOpPasswordEncoder implements PasswordEncoder {
public String encode(CharSequence rawPassword) {
diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoderUtils.java b/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoderUtils.java
new file mode 100644
index 0000000000..aca4b7681d
--- /dev/null
+++ b/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoderUtils.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * 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 org.springframework.security.crypto.password;
+
+import org.springframework.security.crypto.codec.Utf8;
+
+/**
+ * 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;
+
+ int result = expectedLength == actualLength ? 0 : 1;
+ for (int i = 0; i < actualLength; i++) {
+ byte expectedByte = expectedLength <= 0 ? 0 : expectedBytes[i % expectedLength];
+ byte actualByte = actualBytes[i % actualLength];
+ result |= expectedByte ^ actualByte;
+ }
+ return result == 0;
+ }
+
+ private static byte[] bytesUtf8(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ return Utf8.encode(s); // need to check if Utf8.encode() runs in constant time (probably not). This may leak length of string.
+ }
+
+ private PasswordEncoderUtils() {
+ }
+}
diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java
index 90023d91ec..0d5d4c72fe 100644
--- a/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java
+++ b/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.security.crypto.password;
import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
@@ -41,7 +43,7 @@ import static org.springframework.security.crypto.util.EncodingUtils.subArray;
* @since 4.1
*/
public class Pbkdf2PasswordEncoder implements PasswordEncoder {
- private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
+
private static final int DEFAULT_HASH_WIDTH = 256;
private static final int DEFAULT_ITERATIONS = 185000;
@@ -50,6 +52,8 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder {
private final byte[] secret;
private final int hashWidth;
private final int iterations;
+ private String algorithm = SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name();
+ private boolean encodeHashAsBase64;
/**
* Constructs a PBKDF2 password encoder with no additional secret value. There will be
@@ -86,16 +90,56 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder {
this.hashWidth = hashWidth;
}
+ /**
+ * Sets the algorithm to use. See
+ * SecretKeyFactory Algorithms
+ * @param secretKeyFactoryAlgorithm the algorithm to use (i.e.
+ * {@code SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1},
+ * {@code SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256},
+ * {@code SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA512})
+ * @since 5.0
+ */
+ public void setAlgorithm(SecretKeyFactoryAlgorithm secretKeyFactoryAlgorithm) {
+ if(secretKeyFactoryAlgorithm == null) {
+ throw new IllegalArgumentException("secretKeyFactoryAlgorithm cannot be null");
+ }
+ String algorithmName = secretKeyFactoryAlgorithm.name();
+ try {
+ SecretKeyFactory.getInstance(algorithmName);
+ }
+ catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Invalid algorithm '" + algorithmName + "'.", e);
+ }
+ this.algorithm = algorithmName;
+ }
+
+ /**
+ * Sets if the resulting hash should be encoded as Base64. The default is false which
+ * means it will be encoded in Hex.
+ * @param encodeHashAsBase64 true if encode as Base64, false if should use Hex
+ * (default)
+ */
+ public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
+ this.encodeHashAsBase64 = encodeHashAsBase64;
+ }
+
@Override
public String encode(CharSequence rawPassword) {
byte[] salt = this.saltGenerator.generateKey();
byte[] encoded = encode(rawPassword, salt);
- return String.valueOf(Hex.encode(encoded));
+ return encode(encoded);
+ }
+
+ private String encode(byte[] bytes) {
+ if(this.encodeHashAsBase64) {
+ return Base64.getEncoder().encodeToString(bytes);
+ }
+ return String.valueOf(Hex.encode(bytes));
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
- byte[] digested = Hex.decode(encodedPassword);
+ byte[] digested = decode(encodedPassword);
byte[] salt = subArray(digested, 0, this.saltGenerator.getKeyLength());
return matches(digested, encode(rawPassword, salt));
}
@@ -115,15 +159,33 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder {
return result == 0;
}
+ private byte[] decode(String encodedBytes) {
+ if(this.encodeHashAsBase64) {
+ return Base64.getDecoder().decode(encodedBytes);
+ }
+ return Hex.decode(encodedBytes);
+ }
+
private byte[] encode(CharSequence rawPassword, byte[] salt) {
try {
PBEKeySpec spec = new PBEKeySpec(rawPassword.toString().toCharArray(),
concatenate(salt, this.secret), this.iterations, this.hashWidth);
- SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
+ SecretKeyFactory skf = SecretKeyFactory.getInstance(this.algorithm);
return concatenate(salt, skf.generateSecret(spec).getEncoded());
}
catch (GeneralSecurityException e) {
throw new IllegalStateException("Could not create hash", e);
}
}
+
+ /**
+ * The Algorithm used for creating the {@link SecretKeyFactory}
+ *
+ * @since 5.0
+ */
+ public enum SecretKeyFactoryAlgorithm {
+ PBKDF2WithHmacSHA1,
+ PBKDF2WithHmacSHA256,
+ PBKDF2WithHmacSHA512
+ }
}
diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java
index 562f09d96b..974f618bb9 100644
--- a/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java
+++ b/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java
@@ -24,6 +24,9 @@ import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
/**
+ * This {@link PasswordEncoder} is provided for legacy purposes only and is not considered
+ * secure.
+ *
* A standard {@code PasswordEncoder} implementation that uses SHA-256 hashing with 1024
* iterations and a random 8-byte random salt value. It uses an additional system-wide
* secret value to provide additional protection.
@@ -37,7 +40,13 @@ import org.springframework.security.crypto.keygen.KeyGenerators;
*
* @author Keith Donald
* @author Luke Taylor
+ * @deprecated Digest based password encoding is not considered secure. Instead use an
+ * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
+ * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
+ * password upgrades. There are no plans to remove this support. It is deprecated to indicate
+ * that this is a legacy implementation and using it is considered insecure.
*/
+@Deprecated
public final class StandardPasswordEncoder implements PasswordEncoder {
private final Digester digester;
diff --git a/crypto/src/test/java/org/springframework/security/crypto/codec/HexTests.java b/crypto/src/test/java/org/springframework/security/crypto/codec/HexTests.java
index 8e8018308f..e464d7171e 100644
--- a/crypto/src/test/java/org/springframework/security/crypto/codec/HexTests.java
+++ b/crypto/src/test/java/org/springframework/security/crypto/codec/HexTests.java
@@ -34,7 +34,7 @@ public class HexTests {
@Test
public void encode() {
assertThat(Hex.encode(new byte[] { (byte) 'A', (byte) 'B', (byte) 'C',
- (byte) 'D' })).isEqualTo(new char[] {'4','1','4','2','4','3','4','4'});
+ (byte) 'D' })).isEqualTo(new char[] {'4', '1', '4', '2', '4', '3', '4', '4'});
}
@Test
diff --git a/crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java b/crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java
new file mode 100644
index 0000000000..9bf0dae34f
--- /dev/null
+++ b/crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * 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 org.springframework.security.crypto.factory;
+
+import org.junit.Test;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class PasswordEncoderFactoriesTests {
+ private PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
+
+ private String rawPassword = "password";
+
+ @Test
+ public void encodeWhenDefaultThenBCryptUsed() {
+ String encodedPassword = this.encoder.encode(this.rawPassword);
+
+ assertThat(encodedPassword).startsWith("{bcrypt}");
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenBCryptThenWorks() {
+ String encodedPassword = "{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenLdapThenWorks() {
+ String encodedPassword = "{ldap}{SSHA}igvD9lOiTXm16dmOw0YWRb9OjK2ThZvdQku2EQ==";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenMd4ThenWorks() {
+ String encodedPassword = "{MD4}{KYp8/QErWyQemYazZQ8UnWWfbGbkYkVC8qMi0duoA84=}152ce09d3261d2b53cac55b2ea4d1c7a";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenMd5ThenWorks() {
+ String encodedPassword = "{MD5}{aRYR+Yp2xSqtgF+vtjH6jNda6M083iEbP+zCFjLt9IA=}905e382a25eed53e22224223b3581092";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenNoopThenWorks() {
+ String encodedPassword = "{noop}password";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenPbkdf2ThenWorks() {
+ String encodedPassword = "{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenSCryptThenWorks() {
+ String encodedPassword = "{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenSHA1ThenWorks() {
+ String encodedPassword = "{SHA-1}{6581QepZz2qd8jVrT2QYPVtK8DuM2n45dVslmc3UTWc=}4f31573948ddbfb8ac9dd80107dfad13fd8f2454";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenSHA256ThenWorks() {
+ String encodedPassword = "{SHA-256}{UisHp3pFSMqcqrhQsrhR+hspIG0SyMDyDW/XtY+t6nA=}a98efbaf59277bfd1837c33fd4fde67de5bcfd2205bcba0992f6fc32b03a8f88";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+ @Test
+ public void matchesWhenSha256ThenWorks() {
+ String encodedPassword = "{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0";
+ assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+ }
+
+}
diff --git a/crypto/src/test/java/org/springframework/security/crypto/keygen/Base64StringKeyGeneratorTests.java b/crypto/src/test/java/org/springframework/security/crypto/keygen/Base64StringKeyGeneratorTests.java
new file mode 100644
index 0000000000..e199143b52
--- /dev/null
+++ b/crypto/src/test/java/org/springframework/security/crypto/keygen/Base64StringKeyGeneratorTests.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * 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 org.springframework.security.crypto.keygen;
+
+import org.junit.Test;
+
+import java.util.Base64;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class Base64StringKeyGeneratorTests {
+ @Test(expected = IllegalArgumentException.class)
+ public void constructorIntWhenLessThan32ThenIllegalArgumentException() {
+ new Base64StringKeyGenerator(31);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void constructorEncoderWhenEncoderNullThenThrowsIllegalArgumentException() {
+ Base64.Encoder encoder = null;
+ new Base64StringKeyGenerator(null);
+ }
+
+ @Test
+ public void generateKeyWhenDefaultConstructorThen32Bytes() {
+ String result = new Base64StringKeyGenerator().generateKey();
+ assertThat(Base64.getDecoder().decode(result.getBytes())).hasSize(32);
+ }
+
+ @Test
+ public void generateKeyWhenCustomKeySizeThen32Bytes() {
+ int size = 40;
+ String result = new Base64StringKeyGenerator(size).generateKey();
+ assertThat(Base64.getDecoder().decode(result.getBytes())).hasSize(size);
+ }
+
+ @Test
+ public void generateKeyWhenBase64Then32Bytes() {
+ String result = new Base64StringKeyGenerator(Base64.getUrlEncoder()).generateKey();
+ assertThat(Base64.getUrlDecoder().decode(result.getBytes())).hasSize(32);
+ }
+
+ @Test
+ public void generateKeyWhenBase64AndCustomKeySizeThen32Bytes() {
+ int size = 40;
+ String result = new Base64StringKeyGenerator(Base64.getUrlEncoder(), size).generateKey();
+ assertThat(Base64.getUrlDecoder().decode(result.getBytes())).hasSize(size);
+ }
+}
diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java
new file mode 100644
index 0000000000..04ec495d22
--- /dev/null
+++ b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * 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 org.springframework.security.crypto.password;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/**
+ * @author Rob Winch
+ * @author Michael Simons
+ * @since 5.0
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DelegatingPasswordEncoderTests {
+ @Mock
+ private PasswordEncoder bcrypt;
+
+ @Mock
+ private PasswordEncoder noop;
+
+ @Mock
+ private PasswordEncoder invalidId;
+
+ private String bcryptId = "bcrypt";
+
+ private String rawPassword = "password";
+
+ private String encodedPassword = "ENCODED-PASSWORD";
+
+ private String bcryptEncodedPassword = "{bcrypt}" + this.encodedPassword;
+
+ private String noopEncodedPassword = "{noop}" + this.encodedPassword;
+
+ private Map+ * TestCase for Md5PasswordEncoder. + *
+ * + * @author colin sampaleanu + * @author Ben Alex + * @author Ray Krueger + * @author Luke Taylor + */ +public class MessageDigestPasswordEncoderTests { + // ~ Methods + // ======================================================================================================== + + @Test + public void md5BasicFunctionality() { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + String raw = "abc123"; + assertThat(pe.matches( raw, "{THIS_IS_A_SALT}a68aafd90299d0b137de28fb4bb68573")).isTrue(); + } + + @Test + public void md5NonAsciiPasswordHasCorrectHash() throws Exception { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + // $ echo -n "??" | md5 + // 7eca689f0d3389d9dea66ae112e5cfd7 + assertThat(pe.matches("\u4F60\u597d", "7eca689f0d3389d9dea66ae112e5cfd7")).isTrue(); + } + + @Test + public void md5Base64() throws Exception { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + pe.setEncodeHashAsBase64(true); + assertThat(pe.matches("abc123", "{THIS_IS_A_SALT}poqv2QKZ0LE33ij7S7aFcw==")).isTrue(); + } + + @Test + public void md5StretchFactorIsProcessedCorrectly() throws Exception { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + pe.setIterations(2); + // Calculate value using: + // echo -n password{salt} | openssl md5 -binary | openssl md5 + assertThat(pe.matches("password", "{salt}eb753fb0c370582b4ee01b30f304b9fc")).isTrue(); + } + + @Test + public void md5MatchesWhenNullSalt() { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + assertThat(pe.matches("password", "5f4dcc3b5aa765d61d8327deb882cf99")).isTrue(); + } + + @Test + public void md5MatchesWhenEmptySalt() { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + assertThat(pe.matches("password", "{}f1026a66095fc2058c1f8771ed05d6da")).isTrue(); + } + + @Test + public void md5MatchesWhenHasSalt() { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + assertThat(pe.matches("password", "{salt}ce421738b1c5540836bdc8ff707f1572")).isTrue(); + } + + @Test + public void md5EncodeThenMatches() { + String rawPassword = "password"; + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + String encode = pe.encode(rawPassword); + assertThat(pe.matches(rawPassword, encode)).isTrue(); + } + + @Test + public void testBasicFunctionality() { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1"); + String raw = "abc123"; + assertThat(pe.matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32")); + + } + + @Test + public void testBase64() throws Exception { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1"); + pe.setEncodeHashAsBase64(true); + String raw = "abc123"; + assertThat(pe.matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32")); + } + + @Test + public void test256() throws Exception { + MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1"); + String raw = "abc123"; + assertThat(pe.matches(raw, "{THIS_IS_A_SALT}4b79b7de23eb23b78cc5ede227d532b8a51f89b2ec166f808af76b0dbedc47d7")); + } + + @Test(expected = IllegalStateException.class) + public void testInvalidStrength() throws Exception { + new MessageDigestPasswordEncoder("SHA-666"); + } +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/PasswordEncoderUtilsTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/PasswordEncoderUtilsTests.java new file mode 100644 index 0000000000..fcf7dbdba2 --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/password/PasswordEncoderUtilsTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * 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 org.springframework.security.crypto.password; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Winch + */ +public class PasswordEncoderUtilsTests { + + @Test + public void equalsWhenDifferentLengthThenFalse() { + assertThat(PasswordEncoderUtils.equals("abc", "a")).isFalse(); + assertThat(PasswordEncoderUtils.equals("a", "abc")).isFalse(); + } + + @Test + public void equalsWhenNullAndNotEmtpyThenFalse() { + assertThat(PasswordEncoderUtils.equals(null, "a")).isFalse(); + assertThat(PasswordEncoderUtils.equals("a", null)).isFalse(); + } + + @Test + public void equalsWhenNullAndNullThenTrue() { + assertThat(PasswordEncoderUtils.equals(null, null)).isTrue(); + } + + @Test + public void equalsWhenNullAndEmptyThenFalse() { + assertThat(PasswordEncoderUtils.equals(null, "")).isFalse(); + assertThat(PasswordEncoderUtils.equals("", null)).isFalse(); + } + + @Test + public void equalsWhenNotEmptyAndEmptyThenFalse() { + assertThat(PasswordEncoderUtils.equals("abc", "")).isFalse(); + assertThat(PasswordEncoderUtils.equals("", "abc")).isFalse(); + } + + @Test + public void equalsWhenEmtpyAndEmptyThenTrue() { + assertThat(PasswordEncoderUtils.equals("", "")).isTrue(); + } + + @Test + public void equalsWhenDifferentCaseThenFalse() { + assertThat(PasswordEncoderUtils.equals("aBc", "abc")).isFalse(); + } + + @Test + public void equalsWhenSameThenTrue() { + assertThat(PasswordEncoderUtils.equals("abcdef", "abcdef")).isTrue(); + } +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java index d641452141..125cd888f8 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java @@ -75,6 +75,42 @@ public class Pbkdf2PasswordEncoderTests { assertThat(fixedHex).isEqualTo(encodedPassword); } + @Test + public void encodeAndMatchWhenBase64ThenSuccess() { + this.encoder.setEncodeHashAsBase64(true); + + String rawPassword = "password"; + String encodedPassword = this.encoder.encode(rawPassword); + assertThat(this.encoder.matches(rawPassword, encodedPassword)).isTrue(); + } + + @Test + public void matchWhenBase64ThenSuccess() { + this.encoder.setEncodeHashAsBase64(true); + String rawPassword = "password"; + String encodedPassword = "3FOwOMcDgxP+z1x/sv184LFY2WVD+ZGMgYP3LPOSmCcDmk1XPYvcCQ=="; + + assertThat(this.encoder.matches(rawPassword, encodedPassword)).isTrue(); + java.util.Base64.getDecoder().decode(encodedPassword); // validate can decode as Base64 + } + + @Test + public void encodeAndMatchWhenSha256ThenSuccess() { + this.encoder.setAlgorithm(Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256); + + String rawPassword = "password"; + String encodedPassword = this.encoder.encode(rawPassword); + assertThat(this.encoder.matches(rawPassword, encodedPassword)).isTrue(); + } + + @Test + public void matchWhenSha256ThenSuccess() { + this.encoder.setAlgorithm(Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256); + + String rawPassword = "password"; + String encodedPassword = "821447f994e2b04c5014e31fa9fca4ae1cc9f2188c4ed53d3ddb5ba7980982b51a0ecebfc0b81a79"; + assertThat(this.encoder.matches(rawPassword, encodedPassword)).isTrue(); + } /** * Used to find the iteration count that takes .5 seconds. */ @@ -105,4 +141,4 @@ public class Pbkdf2PasswordEncoderTests { } System.out.println("Iterations " + iterations); } -} \ No newline at end of file +} diff --git a/crypto/src/test/resources/logback-test.xml b/crypto/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..2d51ba4180 --- /dev/null +++ b/crypto/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ +