Skip to content

Commit b3da1e4

Browse files
simmacrwinch
authored andcommitted
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
1 parent 1b1e45a commit b3da1e4

File tree

7 files changed

+706
-2
lines changed

7 files changed

+706
-2
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.crypto.argon2;
17+
18+
import java.util.Base64;
19+
import org.bouncycastle.crypto.params.Argon2Parameters;
20+
import org.bouncycastle.util.Arrays;
21+
22+
/**
23+
* Utility for encoding and decoding Argon2 hashes.
24+
*
25+
* Used by {@link Argon2PasswordEncoder}.
26+
*
27+
* @author Simeon Macke
28+
* @since 5.3
29+
*/
30+
class Argon2EncodingUtils {
31+
private static final Base64.Encoder b64encoder = Base64.getEncoder().withoutPadding();
32+
private static final Base64.Decoder b64decoder = Base64.getDecoder();
33+
34+
/**
35+
* Encodes a raw Argon2-hash and its parameters into the standard Argon2-hash-string as specified in the reference
36+
* implementation (https://github.com/P-H-C/phc-winner-argon2/blob/master/src/encoding.c#L244):
37+
*
38+
* {@code $argon2<T>[$v=<num>]$m=<num>,t=<num>,p=<num>$<bin>$<bin>}
39+
*
40+
* where {@code <T>} is either 'd', 'id', or 'i', {@code <num>} is a decimal integer (positive,
41+
* fits in an 'unsigned long'), and {@code <bin>} is Base64-encoded data (no '=' padding
42+
* characters, no newline or whitespace).
43+
*
44+
* The last two binary chunks (encoded in Base64) are, in that order,
45+
* the salt and the output. If no salt has been used, the salt will be omitted.
46+
*
47+
* @param hash the raw Argon2 hash in binary format
48+
* @param parameters the Argon2 parameters that were used to create the hash
49+
* @return the encoded Argon2-hash-string as described above
50+
* @throws IllegalArgumentException if the Argon2Parameters are invalid
51+
*/
52+
public static String encode(byte[] hash, Argon2Parameters parameters) throws IllegalArgumentException {
53+
StringBuilder stringBuilder = new StringBuilder();
54+
55+
switch (parameters.getType()) {
56+
case Argon2Parameters.ARGON2_d: stringBuilder.append("$argon2d"); break;
57+
case Argon2Parameters.ARGON2_i: stringBuilder.append("$argon2i"); break;
58+
case Argon2Parameters.ARGON2_id: stringBuilder.append("$argon2id"); break;
59+
default: throw new IllegalArgumentException("Invalid algorithm type: "+parameters.getType());
60+
}
61+
stringBuilder.append("$v=").append(parameters.getVersion())
62+
.append("$m=").append(parameters.getMemory())
63+
.append(",t=").append(parameters.getIterations())
64+
.append(",p=").append(parameters.getLanes());
65+
66+
if (parameters.getSalt() != null) {
67+
stringBuilder.append("$")
68+
.append(b64encoder.encodeToString(parameters.getSalt()));
69+
}
70+
71+
stringBuilder.append("$")
72+
.append(b64encoder.encodeToString(hash));
73+
74+
return stringBuilder.toString();
75+
}
76+
77+
/**
78+
* Decodes an Argon2 hash string as specified in the reference implementation
79+
* (https://github.com/P-H-C/phc-winner-argon2/blob/master/src/encoding.c#L244) into the raw hash and the used
80+
* parameters.
81+
*
82+
* The hash has to be formatted as follows:
83+
* {@code $argon2<T>[$v=<num>]$m=<num>,t=<num>,p=<num>$<bin>$<bin>}
84+
*
85+
* where {@code <T>} is either 'd', 'id', or 'i', {@code <num>} is a decimal integer (positive,
86+
* fits in an 'unsigned long'), and {@code <bin>} is Base64-encoded data (no '=' padding
87+
* characters, no newline or whitespace).
88+
*
89+
* The last two binary chunks (encoded in Base64) are, in that order,
90+
* the salt and the output. Both are required. The binary salt length and the
91+
* output length must be in the allowed ranges defined in argon2.h.
92+
* @param encodedHash the Argon2 hash string as described above
93+
* @return an {@link Argon2Hash} object containing the raw hash and the {@link Argon2Parameters}.
94+
* @throws IllegalArgumentException if the encoded hash is malformed
95+
*/
96+
public static Argon2Hash decode(String encodedHash) throws IllegalArgumentException {
97+
Argon2Parameters.Builder paramsBuilder;
98+
99+
String[] parts = encodedHash.split("\\$");
100+
101+
if (parts.length < 4) {
102+
throw new IllegalArgumentException("Invalid encoded Argon2-hash");
103+
}
104+
105+
int currentPart = 1;
106+
107+
switch (parts[currentPart++]) {
108+
case "argon2d": paramsBuilder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_d); break;
109+
case "argon2i": paramsBuilder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i); break;
110+
case "argon2id": paramsBuilder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id); break;
111+
default: throw new IllegalArgumentException("Invalid algorithm type: "+parts[0]);
112+
}
113+
114+
if (parts[currentPart].startsWith("v=")) {
115+
paramsBuilder.withVersion(Integer.parseInt(parts[currentPart].substring(2)));
116+
currentPart++;
117+
}
118+
119+
String[] performanceParams = parts[currentPart++].split(",");
120+
121+
if (performanceParams.length != 3) {
122+
throw new IllegalArgumentException("Amount of performance parameters invalid");
123+
}
124+
125+
if (performanceParams[0].startsWith("m=")) {
126+
paramsBuilder.withMemoryAsKB(Integer.parseInt(performanceParams[0].substring(2)));
127+
} else {
128+
throw new IllegalArgumentException("Invalid memory parameter");
129+
}
130+
131+
if (performanceParams[1].startsWith("t=")) {
132+
paramsBuilder.withIterations(Integer.parseInt(performanceParams[1].substring(2)));
133+
} else {
134+
throw new IllegalArgumentException("Invalid iterations parameter");
135+
}
136+
137+
if (performanceParams[2].startsWith("p=")) {
138+
paramsBuilder.withParallelism(Integer.parseInt(performanceParams[2].substring(2)));
139+
} else {
140+
throw new IllegalArgumentException("Invalid parallelity parameter");
141+
}
142+
143+
paramsBuilder.withSalt(b64decoder.decode(parts[currentPart++]));
144+
145+
return new Argon2Hash(b64decoder.decode(parts[currentPart]), paramsBuilder.build());
146+
}
147+
148+
public static class Argon2Hash {
149+
150+
private byte[] hash;
151+
private Argon2Parameters parameters;
152+
153+
Argon2Hash(byte[] hash, Argon2Parameters parameters) {
154+
this.hash = Arrays.clone(hash);
155+
this.parameters = parameters;
156+
}
157+
158+
public byte[] getHash() {
159+
return Arrays.clone(hash);
160+
}
161+
162+
public void setHash(byte[] hash) {
163+
this.hash = Arrays.clone(hash);
164+
}
165+
166+
public Argon2Parameters getParameters() {
167+
return parameters;
168+
}
169+
170+
public void setParameters(Argon2Parameters parameters) {
171+
this.parameters = parameters;
172+
}
173+
}
174+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.crypto.argon2;
18+
19+
import org.apache.commons.logging.Log;
20+
import org.apache.commons.logging.LogFactory;
21+
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
22+
import org.bouncycastle.crypto.params.Argon2Parameters;
23+
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
24+
import org.springframework.security.crypto.keygen.KeyGenerators;
25+
import org.springframework.security.crypto.password.PasswordEncoder;
26+
27+
/**
28+
* <p>
29+
* Implementation of PasswordEncoder that uses the Argon2 hashing function.
30+
* Clients can optionally supply the length of the salt to use, the length
31+
* of the generated hash, a cpu cost parameter, a memory cost parameter
32+
* and a parallelization parameter.
33+
* </p>
34+
*
35+
* <p>Note:</p>
36+
* <p>The currently implementation uses Bouncy castle which does not exploit
37+
* parallelism/optimizations that password crackers will, so there is an
38+
* unnecessary asymmetry between attacker and defender.</p>
39+
*
40+
* @author Simeon Macke
41+
* @since 5.3
42+
*/
43+
public class Argon2PasswordEncoder implements PasswordEncoder {
44+
45+
private static final int DEFAULT_SALT_LENGTH = 16;
46+
private static final int DEFAULT_HASH_LENGTH = 32;
47+
private static final int DEFAULT_PARALLELISM = 1;
48+
private static final int DEFAULT_MEMORY = 1 << 12;
49+
private static final int DEFAULT_ITERATIONS = 3;
50+
51+
private final Log logger = LogFactory.getLog(getClass());
52+
53+
private final int hashLength;
54+
private final int parallelism;
55+
private final int memory;
56+
private final int iterations;
57+
58+
private final BytesKeyGenerator saltGenerator;
59+
60+
public Argon2PasswordEncoder(int saltLength, int hashLength, int parallelism, int memory, int iterations) {
61+
this.hashLength = hashLength;
62+
this.parallelism = parallelism;
63+
this.memory = memory;
64+
this.iterations = iterations;
65+
66+
this.saltGenerator = KeyGenerators.secureRandom(saltLength);
67+
}
68+
69+
public Argon2PasswordEncoder() {
70+
this(DEFAULT_SALT_LENGTH, DEFAULT_HASH_LENGTH, DEFAULT_PARALLELISM, DEFAULT_MEMORY, DEFAULT_ITERATIONS);
71+
}
72+
73+
@Override
74+
public String encode(CharSequence rawPassword) {
75+
byte[] salt = saltGenerator.generateKey();
76+
byte[] hash = new byte[hashLength];
77+
78+
Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id).
79+
withSalt(salt).
80+
withParallelism(parallelism).
81+
withMemoryAsKB(memory).
82+
withIterations(iterations).
83+
build();
84+
Argon2BytesGenerator generator = new Argon2BytesGenerator();
85+
generator.init(params);
86+
generator.generateBytes(rawPassword.toString().toCharArray(), hash);
87+
88+
return Argon2EncodingUtils.encode(hash, params);
89+
}
90+
91+
@Override
92+
public boolean matches(CharSequence rawPassword, String encodedPassword) {
93+
if (encodedPassword == null) {
94+
logger.warn("password hash is null");
95+
return false;
96+
}
97+
98+
Argon2EncodingUtils.Argon2Hash decoded;
99+
100+
try {
101+
decoded = Argon2EncodingUtils.decode(encodedPassword);
102+
} catch (IllegalArgumentException e) {
103+
logger.warn("Malformed password hash", e);
104+
return false;
105+
}
106+
107+
byte[] hashBytes = new byte[decoded.getHash().length];
108+
109+
Argon2BytesGenerator generator = new Argon2BytesGenerator();
110+
generator.init(decoded.getParameters());
111+
generator.generateBytes(rawPassword.toString().toCharArray(), hashBytes);
112+
113+
return constantTimeArrayEquals(decoded.getHash(), hashBytes);
114+
}
115+
116+
@Override
117+
public boolean upgradeEncoding(String encodedPassword) {
118+
if (encodedPassword == null || encodedPassword.length() == 0) {
119+
logger.warn("password hash is null");
120+
return false;
121+
}
122+
123+
Argon2Parameters parameters = Argon2EncodingUtils.decode(encodedPassword).getParameters();
124+
125+
return parameters.getMemory() < this.memory || parameters.getIterations() < this.iterations;
126+
}
127+
128+
private static boolean constantTimeArrayEquals(byte[] expected, byte[] actual) {
129+
if (expected.length != actual.length) {
130+
return false;
131+
}
132+
133+
int result = 0;
134+
for (int i = 0; i < expected.length; i++) {
135+
result |= expected[i] ^ actual[i];
136+
}
137+
return result == 0;
138+
}
139+
140+
}

crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.security.crypto.factory;
1818

19+
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
1920
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
2021
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
2122
import org.springframework.security.crypto.password.PasswordEncoder;
@@ -49,6 +50,7 @@ public class PasswordEncoderFactories {
4950
* <li>SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}</li>
5051
* <li>SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}</li>
5152
* <li>sha256 - {@link org.springframework.security.crypto.password.StandardPasswordEncoder}</li>
53+
* <li>argon2 - {@link Argon2PasswordEncoder}</li>
5254
* </ul>
5355
*
5456
* @return the {@link PasswordEncoder} to use
@@ -67,6 +69,7 @@ public static PasswordEncoder createDelegatingPasswordEncoder() {
6769
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
6870
encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
6971
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
72+
encoders.put("argon2", new Argon2PasswordEncoder());
7073

7174
return new DelegatingPasswordEncoder(encodingId, encoders);
7275
}

0 commit comments

Comments
 (0)