From d9a594d039f97aad26a881a94cff1f7105c16c6b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sun, 22 Oct 2017 13:52:55 -0500 Subject: [PATCH] Add Md4PasswordEncoder to crypto Issue: gh-4674 --- .../security/crypto/password/Md4.java | 182 ++++++++++++++++++ .../crypto/password/Md4PasswordEncoder.java | 115 +++++++++++ .../password/Md4PasswordEncoderTests.java | 70 +++++++ 3 files changed, 367 insertions(+) create mode 100644 crypto/src/main/java/org/springframework/security/crypto/password/Md4.java create mode 100644 crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java create mode 100644 crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java 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..e19598e19f --- /dev/null +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java @@ -0,0 +1,115 @@ +/* + * 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 MD4. + * + * @author Ray Krueger + * @author Luke Taylor + * @since 1.0.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. + */ +@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/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java new file mode 100644 index 0000000000..30433cab49 --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.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 static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + + +public class Md4PasswordEncoderTests { + + @Test + public void testEncodeUnsaltedPassword() { + Md4PasswordEncoder md4 = new Md4PasswordEncoder(); + md4.setEncodeHashAsBase64(true); + assertThat(md4.matches("ww_uni123", "8zobtq72iAt0W6KNqavGwg==")).isTrue(); + } + + @Test + public void testEncodeSaltedPassword() { + Md4PasswordEncoder md4 = new Md4PasswordEncoder(); + md4.setEncodeHashAsBase64(true); + assertThat(md4.matches("ww_uni123", "{Alan K Stewart}ZplT6P5Kv6Rlu6W4FIoYNA==")).isTrue(); + } + + @Test + public void testEncodeNullPassword() { + Md4PasswordEncoder md4 = new Md4PasswordEncoder(); + md4.setEncodeHashAsBase64(true); + assertThat(md4.matches(null, "MdbP4NFq6TG3PFnX4MCJwA==")).isTrue(); + } + + @Test + public void testEncodeEmptyPassword() { + Md4PasswordEncoder md4 = new Md4PasswordEncoder(); + md4.setEncodeHashAsBase64(true); + assertThat(md4.matches(null, "MdbP4NFq6TG3PFnX4MCJwA==")).isTrue(); + } + + @Test + public void testNonAsciiPasswordHasCorrectHash() { + Md4PasswordEncoder md4 = new Md4PasswordEncoder(); + assertThat(md4.matches("\u4F60\u597d", "a7f1196539fd1f85f754ffd185b16e6e")).isTrue(); + } + + @Test + public void testEncodedMatches() { + String rawPassword = "password"; + Md4PasswordEncoder md4 = new Md4PasswordEncoder(); + String encodedPassword = md4.encode(rawPassword); + + assertThat(md4.matches(rawPassword, encodedPassword)).isTrue(); + } +} +