blob: 2a8fdee40b6b8fa0dcff4f697e29f6eb6ed419d1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.apache.shiro.crypto.support.hashes.argon2;
import org.apache.shiro.crypto.hash.HashRequest;
import org.apache.shiro.crypto.hash.HashSpi;
import org.apache.shiro.lang.util.ByteSource;
import org.apache.shiro.lang.util.SimpleByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
/**
* A HashProvider for the Argon2 hash algorithm.
*
* <p>This class is intended to be used by the {@code HashProvider} class from Shiro. However,
* this class can also be used to created instances of the Argon2 hash manually.</p>
*
* <p>Furthermore, there is a nested {@link Parameters} class which provides names for the
* keys used in the parameters map of the {@link HashRequest} class.</p>
*
* @since 2.0
*/
public class Argon2HashProvider implements HashSpi {
private static final Logger LOG = LoggerFactory.getLogger(Argon2HashProvider.class);
@Override
public Set<String> getImplementedAlgorithms() {
return Argon2Hash.getAlgorithmsArgon2();
}
@Override
public Argon2Hash fromString(String format) {
return Argon2Hash.fromString(format);
}
@Override
public HashFactory newHashFactory(Random random) {
return new Argon2HashFactory(random);
}
static class Argon2HashFactory implements HashSpi.HashFactory {
private final SecureRandom random;
public Argon2HashFactory(Random random) {
if (!(random instanceof SecureRandom)) {
throw new IllegalArgumentException("Only SecureRandom instances are supported at the moment!");
}
this.random = (SecureRandom) random;
}
@Override
public Argon2Hash generate(HashRequest hashRequest) {
final String algorithmName = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ALGORITHM_NAME))
.map(algo -> (String) algo)
.orElse(Parameters.DEFAULT_ALGORITHM_NAME);
final int version = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ALGORITHM_VERSION))
.flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_ALGORITHM_VERSION))
.orElse(Parameters.DEFAULT_ALGORITHM_VERSION);
final ByteSource salt = parseSalt(hashRequest);
final int iterations = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ITERATIONS))
.flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_ITERATIONS))
.orElse(Parameters.DEFAULT_ITERATIONS);
final int memoryKib = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_MEMORY_KIB))
.flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_MEMORY_KIB))
.orElse(Parameters.DEFAULT_MEMORY_KIB);
final int parallelism = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_PARALLELISM))
.flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_PARALLELISM))
.orElse(Parameters.DEFAULT_PARALLELISM);
final int outputLengthBits = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_OUTPUT_LENGTH_BITS))
.flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_OUTPUT_LENGTH_BITS))
.orElse(Parameters.DEFAULT_OUTPUT_LENGTH_BITS);
return Argon2Hash.generate(
algorithmName,
version,
hashRequest.getSource(),
salt,
iterations,
memoryKib,
parallelism,
outputLengthBits
);
}
private ByteSource parseSalt(HashRequest hashRequest) {
return Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_SALT))
.map(saltParm -> Base64.getDecoder().decode((String) saltParm))
.map(SimpleByteSource::new)
.flatMap(this::lengthValidOrEmpty)
.orElseGet(() -> Argon2Hash.createSalt(random));
}
private Optional<ByteSource> lengthValidOrEmpty(ByteSource bytes) {
if (bytes.getBytes().length != 16) {
return Optional.empty();
}
return Optional.of(bytes);
}
private Optional<Integer> intOrEmpty(Object maybeInt, String parameterName) {
try {
return Optional.of(Integer.parseInt((String) maybeInt, 10));
} catch (NumberFormatException numberFormatException) {
String message = String.format(
Locale.ENGLISH,
"Expected Integer for parameter %s, but %s is not parsable.",
parameterName, maybeInt
);
LOG.warn(message, numberFormatException);
return Optional.empty();
}
}
}
/**
* Parameters for the {@link Argon2Hash} class.
*
* <p>This class contains public constants only. The constants starting with {@code PARAMETER_} are
* the parameter names recognized by the
* {@link org.apache.shiro.crypto.hash.HashSpi.HashFactory#generate(HashRequest)} method.</p>
*
* <p>The constants starting with {@code DEFAULT_} are their respective default values.</p>
*/
public static final class Parameters {
public static final String DEFAULT_ALGORITHM_NAME = Argon2Hash.DEFAULT_ALGORITHM_NAME;
public static final int DEFAULT_ALGORITHM_VERSION = Argon2Hash.DEFAULT_ALGORITHM_VERSION;
public static final int DEFAULT_ITERATIONS = Argon2Hash.DEFAULT_ITERATIONS;
public static final int DEFAULT_MEMORY_KIB = Argon2Hash.DEFAULT_MEMORY_KIB;
public static final int DEFAULT_PARALLELISM = Argon2Hash.DEFAULT_PARALLELISM;
public static final int DEFAULT_OUTPUT_LENGTH_BITS = Argon2Hash.DEFAULT_OUTPUT_LENGTH_BITS;
/**
* Parameter for modifying the internal algorithm used by Argon2.
*
* <p>Valid values are {@code argon2i} (optimized to resist side-channel attacks),
* {@code argon2d} (maximizes resistance to GPU cracking attacks)
* and {@code argon2id} (a hybrid version).</p>
*
* <p>The default value is {@value DEFAULT_ALGORITHM_NAME} when this parameter is not specified.</p>
*/
public static final String PARAMETER_ALGORITHM_NAME = "Argon2.algorithmName";
public static final String PARAMETER_ALGORITHM_VERSION = "Argon2.version";
/**
* The salt to use.
*
* <p>The value for this parameter accepts a Base64-encoded 16byte (128bit) salt.</p>
*
* <p>As for any KDF, do not use a static salt value for multiple passwords.</p>
*
* <p>The default value is a new random 128bit-salt, if this parameter is not specified.</p>
*/
public static final String PARAMETER_SALT = "Argon2.salt";
public static final String PARAMETER_ITERATIONS = "Argon2.iterations";
public static final String PARAMETER_MEMORY_KIB = "Argon2.memoryKib";
public static final String PARAMETER_PARALLELISM = "Argon2.parallelism";
/**
* The output length (in bits) of the resulting data section.
*
* <p>Argon2 allows to modify the length of the generated output.</p>
*
* <p>The default value is {@value DEFAULT_OUTPUT_LENGTH_BITS} when this parameter is not specified.</p>
*/
public static final String PARAMETER_OUTPUT_LENGTH_BITS = "Argon2.outputLength";
private Parameters() {
// utility class
}
}
}