Bläddra i källkod

PasswordEncoders from spring

Crystal.Sea 4 år sedan
förälder
incheckning
c2537813b5

+ 2 - 2
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

+ 5 - 4
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;

+ 211 - 0
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 <tt>forceLowerCasePrefix</tt> 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;
+    }
+}

+ 184 - 0
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);
+    }
+}
+

+ 154 - 0
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:
+ *
+ * <pre>
+ * s = salt == null ? "" : "{" + salt + "}"
+ * s + md4(password + s)
+ * </pre>
+ *
+ * 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":
+ *
+ * <pre>
+ * String s = salt == null ? "" : "{" + salt + "}";
+ * s + md4(password + s)
+ * "{thisissalt}" + md4(password + "{thisissalt}")
+ * "{thisissalt}6cc7924dad12ade79dfb99e424f25260"
+ * </pre>
+ *
+ * If the salt does not exist, then omit "{salt}" like this:
+ *
+ * <pre>
+ * md4(password)
+ * </pre>
+ *
+ * If the salt is an empty String, then only use "{}" like this:
+ *
+ * <pre>
+ * "{}" + md4(password + "{}")
+ * </pre>
+ *
+ * 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:
+ *
+ * <pre>
+ * String salt = saltSource.getSalt(user);
+ * String s = salt == null ? null : "{" + salt + "}";
+ * String migratedPassword = s + user.getPassword();
+ * </pre>
+ *
+ * @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);
+    }
+}

+ 176 - 0
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:
+ *
+ * <pre>
+ * s = salt == null ? "" : "{" + salt + "}"
+ * s + digest(password + s)
+ * </pre>
+ *
+ * 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":
+ *
+ * <pre>
+ * String s = salt == null ? "" : "{" + salt + "}";
+ * s + md5(password + s)
+ * "{thisissalt}" + md5(password + "{thisissalt}")
+ * "{thisissalt}2a4e7104c2780098f50ed5a84bb2323d"
+ * </pre>
+ *
+ * If the salt does not exist, then omit "{salt}" like this:
+ *
+ * <pre>
+ * digest(password)
+ * </pre>
+ *
+ * If the salt is an empty String, then only use "{}" like this:
+ *
+ * <pre>
+ * "{}" + digest(password + "{}")
+ * </pre>
+ *
+ * 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:
+ *
+ * <pre>
+ * String salt = saltSource.getSalt(user);
+ * String s = salt == null ? null : "{" + salt + "}";
+ * String migratedPassword = s + user.getPassword();
+ * </pre>
+ *
+ * @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
+     * <a href="https://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html#AppA">
+     * Message Digest Algorithms</a> 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);
+    }
+}

+ 58 - 0
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() {
+    }
+
+}

+ 38 - 0
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() {
+    }
+}