Add Argon2PasswordEncoder

Add PasswordEncoder for the Argon2 hashing algorithm (Password Hashing
Competition (PHC) winner).
This implementation uses the BouncyCastle-implementation of Argon2.

Fixes gh-5354
This commit is contained in:
Simeon Macke 2019-06-27 13:44:15 +02:00 committed by Rob Winch
parent 1b1e45a1ef
commit b3da1e466b
7 changed files with 706 additions and 2 deletions

View File

@ -0,0 +1,174 @@
/*
* Copyright 2002-2019 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.
*/
package org.springframework.security.crypto.argon2;
import java.util.Base64;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.bouncycastle.util.Arrays;
/**
* Utility for encoding and decoding Argon2 hashes.
*
* Used by {@link Argon2PasswordEncoder}.
*
* @author Simeon Macke
* @since 5.3
*/
class Argon2EncodingUtils {
private static final Base64.Encoder b64encoder = Base64.getEncoder().withoutPadding();
private static final Base64.Decoder b64decoder = Base64.getDecoder();
/**
* Encodes a raw Argon2-hash and its parameters into the standard Argon2-hash-string as specified in the reference
* implementation (https://github.com/P-H-C/phc-winner-argon2/blob/master/src/encoding.c#L244):
*
* {@code $argon2<T>[$v=<num>]$m=<num>,t=<num>,p=<num>$<bin>$<bin>}
*
* where {@code <T>} is either 'd', 'id', or 'i', {@code <num>} is a decimal integer (positive,
* fits in an 'unsigned long'), and {@code <bin>} is Base64-encoded data (no '=' padding
* characters, no newline or whitespace).
*
* The last two binary chunks (encoded in Base64) are, in that order,
* the salt and the output. If no salt has been used, the salt will be omitted.
*
* @param hash the raw Argon2 hash in binary format
* @param parameters the Argon2 parameters that were used to create the hash
* @return the encoded Argon2-hash-string as described above
* @throws IllegalArgumentException if the Argon2Parameters are invalid
*/
public static String encode(byte[] hash, Argon2Parameters parameters) throws IllegalArgumentException {
StringBuilder stringBuilder = new StringBuilder();
switch (parameters.getType()) {
case Argon2Parameters.ARGON2_d: stringBuilder.append("$argon2d"); break;
case Argon2Parameters.ARGON2_i: stringBuilder.append("$argon2i"); break;
case Argon2Parameters.ARGON2_id: stringBuilder.append("$argon2id"); break;
default: throw new IllegalArgumentException("Invalid algorithm type: "+parameters.getType());
}
stringBuilder.append("$v=").append(parameters.getVersion())
.append("$m=").append(parameters.getMemory())
.append(",t=").append(parameters.getIterations())
.append(",p=").append(parameters.getLanes());
if (parameters.getSalt() != null) {
stringBuilder.append("$")
.append(b64encoder.encodeToString(parameters.getSalt()));
}
stringBuilder.append("$")
.append(b64encoder.encodeToString(hash));
return stringBuilder.toString();
}
/**
* Decodes an Argon2 hash string as specified in the reference implementation
* (https://github.com/P-H-C/phc-winner-argon2/blob/master/src/encoding.c#L244) into the raw hash and the used
* parameters.
*
* The hash has to be formatted as follows:
* {@code $argon2<T>[$v=<num>]$m=<num>,t=<num>,p=<num>$<bin>$<bin>}
*
* where {@code <T>} is either 'd', 'id', or 'i', {@code <num>} is a decimal integer (positive,
* fits in an 'unsigned long'), and {@code <bin>} is Base64-encoded data (no '=' padding
* characters, no newline or whitespace).
*
* The last two binary chunks (encoded in Base64) are, in that order,
* the salt and the output. Both are required. The binary salt length and the
* output length must be in the allowed ranges defined in argon2.h.
* @param encodedHash the Argon2 hash string as described above
* @return an {@link Argon2Hash} object containing the raw hash and the {@link Argon2Parameters}.
* @throws IllegalArgumentException if the encoded hash is malformed
*/
public static Argon2Hash decode(String encodedHash) throws IllegalArgumentException {
Argon2Parameters.Builder paramsBuilder;
String[] parts = encodedHash.split("\\$");
if (parts.length < 4) {
throw new IllegalArgumentException("Invalid encoded Argon2-hash");
}
int currentPart = 1;
switch (parts[currentPart++]) {
case "argon2d": paramsBuilder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_d); break;
case "argon2i": paramsBuilder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i); break;
case "argon2id": paramsBuilder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id); break;
default: throw new IllegalArgumentException("Invalid algorithm type: "+parts[0]);
}
if (parts[currentPart].startsWith("v=")) {
paramsBuilder.withVersion(Integer.parseInt(parts[currentPart].substring(2)));
currentPart++;
}
String[] performanceParams = parts[currentPart++].split(",");
if (performanceParams.length != 3) {
throw new IllegalArgumentException("Amount of performance parameters invalid");
}
if (performanceParams[0].startsWith("m=")) {
paramsBuilder.withMemoryAsKB(Integer.parseInt(performanceParams[0].substring(2)));
} else {
throw new IllegalArgumentException("Invalid memory parameter");
}
if (performanceParams[1].startsWith("t=")) {
paramsBuilder.withIterations(Integer.parseInt(performanceParams[1].substring(2)));
} else {
throw new IllegalArgumentException("Invalid iterations parameter");
}
if (performanceParams[2].startsWith("p=")) {
paramsBuilder.withParallelism(Integer.parseInt(performanceParams[2].substring(2)));
} else {
throw new IllegalArgumentException("Invalid parallelity parameter");
}
paramsBuilder.withSalt(b64decoder.decode(parts[currentPart++]));
return new Argon2Hash(b64decoder.decode(parts[currentPart]), paramsBuilder.build());
}
public static class Argon2Hash {
private byte[] hash;
private Argon2Parameters parameters;
Argon2Hash(byte[] hash, Argon2Parameters parameters) {
this.hash = Arrays.clone(hash);
this.parameters = parameters;
}
public byte[] getHash() {
return Arrays.clone(hash);
}
public void setHash(byte[] hash) {
this.hash = Arrays.clone(hash);
}
public Argon2Parameters getParameters() {
return parameters;
}
public void setParameters(Argon2Parameters parameters) {
this.parameters = parameters;
}
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright 2002-2019 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.
*/
package org.springframework.security.crypto.argon2;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* <p>
* Implementation of PasswordEncoder that uses the Argon2 hashing function.
* Clients can optionally supply the length of the salt to use, the length
* of the generated hash, a cpu cost parameter, a memory cost parameter
* and a parallelization parameter.
* </p>
*
* <p>Note:</p>
* <p>The currently implementation uses Bouncy castle which does not exploit
* parallelism/optimizations that password crackers will, so there is an
* unnecessary asymmetry between attacker and defender.</p>
*
* @author Simeon Macke
* @since 5.3
*/
public class Argon2PasswordEncoder implements PasswordEncoder {
private static final int DEFAULT_SALT_LENGTH = 16;
private static final int DEFAULT_HASH_LENGTH = 32;
private static final int DEFAULT_PARALLELISM = 1;
private static final int DEFAULT_MEMORY = 1 << 12;
private static final int DEFAULT_ITERATIONS = 3;
private final Log logger = LogFactory.getLog(getClass());
private final int hashLength;
private final int parallelism;
private final int memory;
private final int iterations;
private final BytesKeyGenerator saltGenerator;
public Argon2PasswordEncoder(int saltLength, int hashLength, int parallelism, int memory, int iterations) {
this.hashLength = hashLength;
this.parallelism = parallelism;
this.memory = memory;
this.iterations = iterations;
this.saltGenerator = KeyGenerators.secureRandom(saltLength);
}
public Argon2PasswordEncoder() {
this(DEFAULT_SALT_LENGTH, DEFAULT_HASH_LENGTH, DEFAULT_PARALLELISM, DEFAULT_MEMORY, DEFAULT_ITERATIONS);
}
@Override
public String encode(CharSequence rawPassword) {
byte[] salt = saltGenerator.generateKey();
byte[] hash = new byte[hashLength];
Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id).
withSalt(salt).
withParallelism(parallelism).
withMemoryAsKB(memory).
withIterations(iterations).
build();
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(params);
generator.generateBytes(rawPassword.toString().toCharArray(), hash);
return Argon2EncodingUtils.encode(hash, params);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null) {
logger.warn("password hash is null");
return false;
}
Argon2EncodingUtils.Argon2Hash decoded;
try {
decoded = Argon2EncodingUtils.decode(encodedPassword);
} catch (IllegalArgumentException e) {
logger.warn("Malformed password hash", e);
return false;
}
byte[] hashBytes = new byte[decoded.getHash().length];
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(decoded.getParameters());
generator.generateBytes(rawPassword.toString().toCharArray(), hashBytes);
return constantTimeArrayEquals(decoded.getHash(), hashBytes);
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
logger.warn("password hash is null");
return false;
}
Argon2Parameters parameters = Argon2EncodingUtils.decode(encodedPassword).getParameters();
return parameters.getMemory() < this.memory || parameters.getIterations() < this.iterations;
}
private static boolean constantTimeArrayEquals(byte[] expected, byte[] actual) {
if (expected.length != actual.length) {
return false;
}
int result = 0;
for (int i = 0; i < expected.length; i++) {
result |= expected[i] ^ actual[i];
}
return result == 0;
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.security.crypto.factory;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@ -49,6 +50,7 @@ public class PasswordEncoderFactories {
* <li>SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}</li>
* <li>SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}</li>
* <li>sha256 - {@link org.springframework.security.crypto.password.StandardPasswordEncoder}</li>
* <li>argon2 - {@link Argon2PasswordEncoder}</li>
* </ul>
*
* @return the {@link PasswordEncoder} to use
@ -67,6 +69,7 @@ public class PasswordEncoderFactories {
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}

View File

@ -0,0 +1,150 @@
/*
* Copyright 2002-2019 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.
*/
package org.springframework.security.crypto.argon2;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Base64;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.junit.Test;
/**
* @author Simeon Macke
*/
public class Argon2EncodingUtilsTests {
private final Base64.Decoder decoder = Base64.getDecoder();
private TestDataEntry testDataEntry1 = new TestDataEntry(
"$argon2i$v=19$m=1024,t=3,p=2$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs",
new Argon2EncodingUtils.Argon2Hash(decoder.decode("cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs"),
(new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i)).
withVersion(19).withMemoryAsKB(1024).withIterations(3).withParallelism(2).
withSalt("cRdFbCw23gz2Mlxk".getBytes()).build()
));
private TestDataEntry testDataEntry2 = new TestDataEntry(
"$argon2id$v=19$m=333,t=5,p=2$JDR8N3k1QWx0$+PrEoHOHsWkU9lnsxqnOFrWTVEuOh7ZRIUIbe2yUG8FgTYNCWJfHQI09JAAFKzr2JAvoejEpTMghUt0WsntQYA",
new Argon2EncodingUtils.Argon2Hash(decoder.decode("+PrEoHOHsWkU9lnsxqnOFrWTVEuOh7ZRIUIbe2yUG8FgTYNCWJfHQI09JAAFKzr2JAvoejEpTMghUt0WsntQYA"),
(new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)).
withVersion(19).withMemoryAsKB(333).withIterations(5).withParallelism(2).
withSalt("$4|7y5Alt".getBytes()).build()
));
@Test
public void decodeWhenValidEncodedHashWithIThenDecodeCorrectly() throws Exception {
assertArgon2HashEquals(testDataEntry1.decoded, Argon2EncodingUtils.decode(testDataEntry1.encoded));
}
@Test
public void decodeWhenValidEncodedHashWithIDThenDecodeCorrectly() throws Exception {
assertArgon2HashEquals(testDataEntry2.decoded, Argon2EncodingUtils.decode(testDataEntry2.encoded));
}
@Test
public void encodeWhenValidArgumentsWithIThenEncodeToCorrectHash() throws Exception {
assertThat(Argon2EncodingUtils
.encode(testDataEntry1.decoded.getHash(), testDataEntry1.decoded.getParameters()))
.isEqualTo(testDataEntry1.encoded);
}
@Test
public void encodeWhenValidArgumentsWithID2ThenEncodeToCorrectHash() throws Exception {
assertThat(Argon2EncodingUtils
.encode(testDataEntry2.decoded.getHash(), testDataEntry2.decoded.getParameters()))
.isEqualTo(testDataEntry2.encoded);
}
@Test(expected = IllegalArgumentException.class)
public void encodeWhenNonexistingAlgorithmThenThrowException() {
Argon2EncodingUtils.encode(new byte[]{0, 1, 2, 3}, (new Argon2Parameters.Builder(3)).
withVersion(19).withMemoryAsKB(333).withIterations(5).withParallelism(2).build());
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenNotAnArgon2HashThenThrowException() {
Argon2EncodingUtils.decode("notahash");
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenNonexistingAlgorithmThenThrowException() {
Argon2EncodingUtils.decode("$argon2x$v=19$m=1024,t=3,p=2$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs");
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenIllegalVersionParameterThenThrowException() {
Argon2EncodingUtils.decode("$argon2i$v=x$m=1024,t=3,p=2$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs");
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenIllegalMemoryParameterThenThrowException() {
Argon2EncodingUtils.decode("$argon2i$v=19$m=x,t=3,p=2$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs");
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenIllegalIterationsParameterThenThrowException() {
Argon2EncodingUtils.decode("$argon2i$v=19$m=1024,t=x,p=2$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs");
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenIllegalParallelityParameterThenThrowException() {
Argon2EncodingUtils.decode("$argon2i$v=19$m=1024,t=3,p=x$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs");
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenMissingVersionParameterThenThrowException() {
Argon2EncodingUtils.decode("$argon2i$m=1024,t=3,p=x$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs");
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenMissingMemoryParameterThenThrowException() {
Argon2EncodingUtils.decode("$argon2i$v=19$t=3,p=2$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs");
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenMissingIterationsParameterThenThrowException() {
Argon2EncodingUtils.decode("$argon2i$v=19$m=1024,p=2$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs");
}
@Test(expected = IllegalArgumentException.class)
public void decodeWhenMissingParallelityParameterThenThrowException() {
Argon2EncodingUtils.decode("$argon2i$v=19$m=1024,t=3$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs");
}
private void assertArgon2HashEquals(Argon2EncodingUtils.Argon2Hash expected, Argon2EncodingUtils.Argon2Hash actual) {
assertThat(actual.getHash()).isEqualTo(expected.getHash());
assertThat(actual.getParameters().getSalt()).isEqualTo(expected.getParameters().getSalt());
assertThat(actual.getParameters().getType()).isEqualTo(expected.getParameters().getType());
assertThat(actual.getParameters().getVersion())
.isEqualTo(expected.getParameters().getVersion());
assertThat(actual.getParameters().getMemory())
.isEqualTo(expected.getParameters().getMemory());
assertThat(actual.getParameters().getIterations())
.isEqualTo(expected.getParameters().getIterations());
assertThat(actual.getParameters().getLanes())
.isEqualTo(expected.getParameters().getLanes());
}
private static class TestDataEntry {
String encoded;
Argon2EncodingUtils.Argon2Hash decoded;
TestDataEntry(String encoded, Argon2EncodingUtils.Argon2Hash decoded) {
this.encoded = encoded;
this.decoded = decoded;
}
}
}

View File

@ -0,0 +1,214 @@
/*
* Copyright 2002-2019 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.
*/
package org.springframework.security.crypto.argon2;
import static org.assertj.core.api.Assertions.assertThat;
import java.lang.reflect.Field;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
/**
* @author Simeon Macke
*/
@RunWith(MockitoJUnitRunner.class)
public class Argon2PasswordEncoderTests {
@Mock
private BytesKeyGenerator keyGeneratorMock;
private Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
@Test
public void encodeDoesNotEqualPassword() {
String result = encoder.encode("password");
assertThat(result).isNotEqualTo("password");
}
@Test
public void encodeWhenEqualPasswordThenMatches() {
String result = encoder.encode("password");
assertThat(encoder.matches("password", result)).isTrue();
}
@Test
public void encodeWhenEqualWithUnicodeThenMatches() {
String result = encoder.encode("passw\u9292rd");
assertThat(encoder.matches("pass\u9292\u9292rd", result)).isFalse();
assertThat(encoder.matches("passw\u9292rd", result)).isTrue();
}
@Test
public void encodeWhenNotEqualThenNotMatches() {
String result = encoder.encode("password");
assertThat(encoder.matches("bogus", result)).isFalse();
}
@Test
public void encodeWhenEqualPasswordWithCustomParamsThenMatches() {
encoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
String result = encoder.encode("password");
assertThat(encoder.matches("password", result)).isTrue();
}
@Test
public void encodeWhenRanTwiceThenResultsNotEqual() {
String password = "secret";
assertThat(encoder.encode(password)).isNotEqualTo(encoder.encode(password));
}
@Test
public void encodeWhenRanTwiceWithCustomParamsThenNotEquals() {
encoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
String password = "secret";
assertThat(encoder.encode(password)).isNotEqualTo(encoder.encode(password));
}
@Test
public void matchesWhenGeneratedWithDifferentEncoderThenTrue() {
Argon2PasswordEncoder oldEncoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
Argon2PasswordEncoder newEncoder = new Argon2PasswordEncoder();
String password = "secret";
String oldEncodedPassword = oldEncoder.encode(password);
assertThat(newEncoder.matches(password, oldEncodedPassword)).isTrue();
}
@Test
public void matchesWhenEncodedPassIsNullThenFalse() {
assertThat(encoder.matches("password", null)).isFalse();
}
@Test
public void matchesWhenEncodedPassIsEmptyThenFalse() {
assertThat(encoder.matches("password", "")).isFalse();
}
@Test
public void matchesWhenEncodedPassIsBogusThenFalse() {
assertThat(encoder.matches("password", "012345678901234567890123456789")).isFalse();
}
@Test
public void encodeWhenUsingPredictableSaltThenEqualTestHash() throws Exception {
injectPredictableSaltGen();
String hash = encoder.encode("sometestpassword");
assertThat(hash).isEqualTo(
"$argon2id$v=19$m=4096,t=3,p=1$QUFBQUFBQUFBQUFBQUFBQQ$hmmTNyJlwbb6HAvFoHFWF+u03fdb0F2qA+39oPlcAqo");
}
@Test
public void encodeWhenUsingPredictableSaltWithCustomParamsThenEqualTestHash() throws Exception {
encoder = new Argon2PasswordEncoder(16, 32, 4, 512, 5);
injectPredictableSaltGen();
String hash = encoder.encode("sometestpassword");
assertThat(hash).isEqualTo(
"$argon2id$v=19$m=512,t=5,p=4$QUFBQUFBQUFBQUFBQUFBQQ$PNv4C3K50bz3rmON+LtFpdisD7ePieLNq+l5iUHgc1k");
}
@Test
public void upgradeEncodingWhenSameEncodingThenFalse() throws Exception {
String hash = encoder.encode("password");
assertThat(encoder.upgradeEncoding(hash)).isFalse();
}
@Test
public void upgradeEncodingWhenSameStandardParamsThenFalse() throws Exception {
Argon2PasswordEncoder newEncoder = new Argon2PasswordEncoder();
String hash = encoder.encode("password");
assertThat(newEncoder.upgradeEncoding(hash)).isFalse();
}
@Test
public void upgradeEncodingWhenSameCustomParamsThenFalse() throws Exception {
Argon2PasswordEncoder oldEncoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
Argon2PasswordEncoder newEncoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
String hash = oldEncoder.encode("password");
assertThat(newEncoder.upgradeEncoding(hash)).isFalse();
}
@Test
public void upgradeEncodingWhenHashHasLowerMemoryThenTrue() throws Exception {
Argon2PasswordEncoder oldEncoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
Argon2PasswordEncoder newEncoder = new Argon2PasswordEncoder(20, 64, 4, 512, 4);
String hash = oldEncoder.encode("password");
assertThat(newEncoder.upgradeEncoding(hash)).isTrue();
}
@Test
public void upgradeEncodingWhenHashHasLowerIterationsThenTrue() throws Exception {
Argon2PasswordEncoder oldEncoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
Argon2PasswordEncoder newEncoder = new Argon2PasswordEncoder(20, 64, 4, 256, 5);
String hash = oldEncoder.encode("password");
assertThat(newEncoder.upgradeEncoding(hash)).isTrue();
}
@Test
public void upgradeEncodingWhenHashHasHigherParamsThenFalse() throws Exception {
Argon2PasswordEncoder oldEncoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
Argon2PasswordEncoder newEncoder = new Argon2PasswordEncoder(20, 64, 4, 128, 3);
String hash = oldEncoder.encode("password");
assertThat(newEncoder.upgradeEncoding(hash)).isFalse();
}
@Test
public void upgradeEncodingWhenEncodedPassIsNullThenFalse() {
assertThat(encoder.upgradeEncoding(null)).isFalse();
}
@Test
public void upgradeEncodingWhenEncodedPassIsEmptyThenFalse() {
assertThat(encoder.upgradeEncoding("")).isFalse();
}
@Test(expected = IllegalArgumentException.class)
public void upgradeEncodingWhenEncodedPassIsBogusThenThrowException() {
encoder.upgradeEncoding("thisIsNoValidHash");
}
private void injectPredictableSaltGen() throws Exception {
byte[] bytes = new byte[16];
Arrays.fill(bytes, (byte) 0x41);
Mockito.when(keyGeneratorMock.generateKey()).thenReturn(bytes);
//we can't use the @InjectMock-annotation because the salt-generator is set in the constructor
//and Mockito will only inject mocks if they are null
Field saltGen = encoder.getClass().getDeclaredField("saltGenerator");
saltGen.setAccessible(true);
saltGen.set(encoder, keyGeneratorMock);
saltGen.setAccessible(false);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2019 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.
@ -19,7 +19,7 @@ package org.springframework.security.crypto.factory;
import org.junit.Test;
import org.springframework.security.crypto.password.PasswordEncoder;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
@ -98,4 +98,10 @@ public class PasswordEncoderFactoriesTests {
assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
}
@Test
public void matchesWhenArgon2ThenWorks() {
String encodedPassword = "{argon2}$argon2d$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$Li5eBf5XrCz0cuzQRe9oflYqmA/VAzmzichw4ZYrvEU";
assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
}
}

View File

@ -265,6 +265,23 @@ String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
----
[[pe-a2pe]]
== Argon2PasswordEncoder
The `Argon2PasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/Argon2[Argon2] algorithm to hash the passwords.
Argon2 is the winner of the https://en.wikipedia.org/wiki/Password_Hashing_Competition[Password Hashing Competition].
In order to defeat password cracking on custom hardware, Argon2 is a deliberately slow algorithm that requires large amounts of memory.
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system.
The current implementation if the `Argon2PasswordEncoder` requires BouncyCastle.
[source,java]
----
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
----
[[pe-pbkdf2pe]]
== Pbkdf2PasswordEncoder