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:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user