Skip to content

Add Argon2PasswordEncoder #7045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -67,6 +69,7 @@ public static PasswordEncoder createDelegatingPasswordEncoder() {
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);
}
Expand Down
Loading