diff --git a/gradleSetEnv.bat b/gradleSetEnv.bat index ddecb4503..c6b3df05f 100644 --- a/gradleSetEnv.bat +++ b/gradleSetEnv.bat @@ -1,7 +1,7 @@ echo off echo set env -set JAVA_HOME=D:\JavaIDE\jdk1.8.0_91 -set GRADLE_HOME=D:\JavaIDE\gradle-6.5.1 +set JAVA_HOME=D:\IDE\jdk1.8.0_202 +set GRADLE_HOME=D:\IDE\gradle-6.5.1 call %JAVA_HOME%/bin/java -version call %GRADLE_HOME%/bin/gradle -version diff --git a/maxkey-core/src/main/java/org/maxkey/autoconfigure/ApplicationAutoConfiguration.java b/maxkey-core/src/main/java/org/maxkey/autoconfigure/ApplicationAutoConfiguration.java index 5a57a17b3..275e9176d 100644 --- a/maxkey-core/src/main/java/org/maxkey/autoconfigure/ApplicationAutoConfiguration.java +++ b/maxkey-core/src/main/java/org/maxkey/autoconfigure/ApplicationAutoConfiguration.java @@ -31,6 +31,10 @@ import org.maxkey.authn.support.rememberme.JdbcRemeberMeService; import org.maxkey.authn.support.rememberme.RedisRemeberMeService; import org.maxkey.constants.ConstantsProperties; import org.maxkey.crypto.keystore.KeyStoreLoader; +import org.maxkey.crypto.password.LdapShaPasswordEncoder; +import org.maxkey.crypto.password.Md4PasswordEncoder; +import org.maxkey.crypto.password.NoOpPasswordEncoder; +import org.maxkey.crypto.password.MessageDigestPasswordEncoder; import org.maxkey.crypto.password.PasswordReciprocal; import org.maxkey.crypto.password.SM3PasswordEncoder; import org.maxkey.crypto.password.StandardPasswordEncoder; @@ -53,10 +57,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; -import org.springframework.security.crypto.password.LdapShaPasswordEncoder; -import org.springframework.security.crypto.password.Md4PasswordEncoder; -import org.springframework.security.crypto.password.MessageDigestPasswordEncoder; -import org.springframework.security.crypto.password.NoOpPasswordEncoder; + import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; diff --git a/maxkey-core/src/main/java/org/maxkey/crypto/password/LdapShaPasswordEncoder.java b/maxkey-core/src/main/java/org/maxkey/crypto/password/LdapShaPasswordEncoder.java new file mode 100644 index 000000000..8287b0ae8 --- /dev/null +++ b/maxkey-core/src/main/java/org/maxkey/crypto/password/LdapShaPasswordEncoder.java @@ -0,0 +1,211 @@ +package org.maxkey.crypto.password; +/* + * Copyright 2002-2018 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 + * + * https://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. + */ + + +import org.springframework.security.crypto.codec.Utf8; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.security.crypto.password.PasswordEncoder; + +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 + * 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. + */ + +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(), 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/maxkey-core/src/main/java/org/maxkey/crypto/password/Md4.java b/maxkey-core/src/main/java/org/maxkey/crypto/password/Md4.java new file mode 100644 index 000000000..b7f77310a --- /dev/null +++ b/maxkey-core/src/main/java/org/maxkey/crypto/password/Md4.java @@ -0,0 +1,184 @@ +package org.maxkey.crypto.password; + +/* + * 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 + * + * https://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. + */ + +/** + * 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/maxkey-core/src/main/java/org/maxkey/crypto/password/Md4PasswordEncoder.java b/maxkey-core/src/main/java/org/maxkey/crypto/password/Md4PasswordEncoder.java new file mode 100644 index 000000000..9a8e205c6 --- /dev/null +++ b/maxkey-core/src/main/java/org/maxkey/crypto/password/Md4PasswordEncoder.java @@ -0,0 +1,154 @@ +package org.maxkey.crypto.password; + +/* + * Copyright 2002-2018 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 + * + * https://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. + */ + +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 org.springframework.security.crypto.password.PasswordEncoder; + +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.0 + * 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. + */ +public class Md4PasswordEncoder implements PasswordEncoder { + private static final String PREFIX = "{"; + private static final String SUFFIX = "}"; + private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator(); + private boolean encodeHashAsBase64; + + + 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/maxkey-core/src/main/java/org/maxkey/crypto/password/MessageDigestPasswordEncoder.java b/maxkey-core/src/main/java/org/maxkey/crypto/password/MessageDigestPasswordEncoder.java new file mode 100644 index 000000000..9e760406e --- /dev/null +++ b/maxkey-core/src/main/java/org/maxkey/crypto/password/MessageDigestPasswordEncoder.java @@ -0,0 +1,176 @@ +package org.maxkey.crypto.password; + +/* + * Copyright 2002-2018 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 + * + * https://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. + */ + +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 org.springframework.security.crypto.password.PasswordEncoder; + +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 5.0 + * @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. + */ + +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/maxkey-core/src/main/java/org/maxkey/crypto/password/NoOpPasswordEncoder.java b/maxkey-core/src/main/java/org/maxkey/crypto/password/NoOpPasswordEncoder.java new file mode 100644 index 000000000..a752476e6 --- /dev/null +++ b/maxkey-core/src/main/java/org/maxkey/crypto/password/NoOpPasswordEncoder.java @@ -0,0 +1,58 @@ +package org.maxkey.crypto.password; + +import org.springframework.security.crypto.password.PasswordEncoder; + +/* + * Copyright 2011-2016 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 + * + * https://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. + */ + +/** + * 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. 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. + */ + +public final class NoOpPasswordEncoder implements PasswordEncoder { + + public String encode(CharSequence rawPassword) { + return rawPassword.toString(); + } + + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return rawPassword.toString().equals(encodedPassword); + } + + /** + * Get the singleton {@link NoOpPasswordEncoder}. + */ + public static PasswordEncoder getInstance() { + return INSTANCE; + } + + private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder(); + + private NoOpPasswordEncoder() { + } + +} diff --git a/maxkey-core/src/main/java/org/maxkey/crypto/password/PasswordEncoderUtils.java b/maxkey-core/src/main/java/org/maxkey/crypto/password/PasswordEncoderUtils.java new file mode 100644 index 000000000..a6fffb4b2 --- /dev/null +++ b/maxkey-core/src/main/java/org/maxkey/crypto/password/PasswordEncoderUtils.java @@ -0,0 +1,38 @@ +package org.maxkey.crypto.password; + + +import org.springframework.security.crypto.codec.Utf8; + +import java.security.MessageDigest; + +/** + * Utility for constant time comparison to prevent against timing attacks. + * + * @author Rob Winch + */ +public 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); + + return MessageDigest.isEqual(expectedBytes, actualBytes); + } + + 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() { + } +}