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,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();
}
}