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:
parent
1b1e45a1ef
commit
b3da1e466b
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue