소스 검색

Add MessageDigestPasswordEncoder to crypto

Issue: gh-4674
Rob Winch 8 년 전
부모
커밋
8fda55e98f

+ 133 - 0
crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java

@@ -0,0 +1,133 @@
+/*
+ * 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.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+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}
+ *
+ * @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 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="http://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
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() {
+	}
+}

+ 124 - 0
crypto/src/test/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoderTests.java

@@ -0,0 +1,124 @@
+/*
+ * 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 java.security.NoSuchAlgorithmException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+/**
+ * <p>
+ * TestCase for Md5PasswordEncoder.
+ * </p>
+ *
+ * @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");
+	}
+}

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