blob: f73b40a560c88cca296a0bc470e45e6a5dd4fd43 [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.bcrypt;
import org.apache.shiro.crypto.hash.AbstractCryptHash;
import org.apache.shiro.lang.util.ByteSource;
import org.apache.shiro.lang.util.SimpleByteSource;
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.StringJoiner;
import static java.util.Collections.unmodifiableSet;
/**
* @since 2.0
*/
class BCryptHash extends AbstractCryptHash {
private static final long serialVersionUID = 6957869292324606101L;
private static final Logger LOG = LoggerFactory.getLogger(AbstractCryptHash.class);
public static final String DEFAULT_ALGORITHM_NAME = "2y";
public static final int DEFAULT_COST = 10;
public static final int SALT_LENGTH = 16;
private static final Set<String> ALGORITHMS_BCRYPT = new HashSet<>(Arrays.asList("2", "2a", "2b", "2y"));
private final int cost;
private final int iterations;
public BCryptHash(final String version, final byte[] hashedData, final ByteSource salt, final int cost) {
super(version, hashedData, salt);
this.cost = cost;
this.iterations = (int) Math.pow(2, cost);
checkValidCost();
}
@Override
protected final void checkValidAlgorithm() {
if (!ALGORITHMS_BCRYPT.contains(getAlgorithmName())) {
final String message = String.format(
Locale.ENGLISH,
"Given algorithm name [%s] not valid for bcrypt. " +
"Valid algorithms: [%s].",
getAlgorithmName(),
ALGORITHMS_BCRYPT
);
throw new IllegalArgumentException(message);
}
}
protected final void checkValidCost() {
checkValidCost(this.cost);
}
public static int checkValidCost(final int cost) {
if (cost < 4 || cost > 31) {
final String message = String.format(
Locale.ENGLISH,
"Expected bcrypt cost >= 4 and <=30, but was [%d].",
cost
);
throw new IllegalArgumentException(message);
}
return cost;
}
public int getCost() {
return this.cost;
}
public static Set<String> getAlgorithmsBcrypt() {
return unmodifiableSet(ALGORITHMS_BCRYPT);
}
public static BCryptHash fromString(String input) {
// the input string should look like this:
// $2y$cost$salt{22}hash
if (!input.startsWith("$")) {
throw new IllegalArgumentException("Unsupported input: " + input);
}
final String[] parts = AbstractCryptHash.DELIMITER.split(input.substring(1));
if (parts.length != 3) {
throw new IllegalArgumentException("Expected string containing three '$' but got: '" + Arrays.toString(parts) + "'.");
}
final String algorithmName = parts[0].trim();
final int cost = Integer.parseInt(parts[1].trim(), 10);
final String dataSection = parts[2];
final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default();
final String saltBase64 = dataSection.substring(0, 22);
final String bytesBase64 = dataSection.substring(22);
final byte[] salt = bcryptBase64.decode(saltBase64.getBytes(StandardCharsets.ISO_8859_1));
final byte[] hashedData = bcryptBase64.decode(bytesBase64.getBytes(StandardCharsets.ISO_8859_1));
return new BCryptHash(algorithmName, hashedData, new SimpleByteSource(salt), cost);
}
public static BCryptHash generate(final ByteSource source) {
return generate(source, createSalt(), DEFAULT_COST);
}
public static BCryptHash generate(final ByteSource source, final ByteSource initialSalt, final int cost) {
return generate(DEFAULT_ALGORITHM_NAME, source, initialSalt, cost);
}
public static BCryptHash generate(String algorithmName, ByteSource source, ByteSource salt, int cost) {
checkValidCost(cost);
final String cryptString = OpenBSDBCrypt.generate(algorithmName, source.getBytes(), salt.getBytes(), cost);
return fromString(cryptString);
}
protected static ByteSource createSalt() {
return createSalt(new SecureRandom());
}
protected static ByteSource createSalt(SecureRandom random) {
return new SimpleByteSource(random.generateSeed(SALT_LENGTH));
}
@Override
public int getSaltLength() {
return SALT_LENGTH;
}
@Override
public String formatToCryptString() {
OpenBSDBase64.Default bsdBase64 = new OpenBSDBase64.Default();
String saltBase64 = new String(bsdBase64.encode(this.getSalt().getBytes()), StandardCharsets.ISO_8859_1);
String dataBase64 = new String(bsdBase64.encode(this.getBytes()), StandardCharsets.ISO_8859_1);
return new StringJoiner("$", "$", "")
.add(this.getAlgorithmName())
.add("" + this.cost)
.add(saltBase64 + dataBase64)
.toString();
}
@Override
public int getIterations() {
return this.iterations;
}
@Override
public boolean matchesPassword(ByteSource plaintextBytes) {
try {
final String cryptString = OpenBSDBCrypt.generate(this.getAlgorithmName(), plaintextBytes.getBytes(), this.getSalt().getBytes(), this.getCost());
BCryptHash other = fromString(cryptString);
return this.equals(other);
} catch (IllegalArgumentException illegalArgumentException) {
// cannot recreate hash. Do not log password.
LOG.warn("Cannot recreate a hash using the same parameters.", illegalArgumentException);
return false;
}
}
@Override
public String toString() {
return new StringJoiner(", ", BCryptHash.class.getSimpleName() + "[", "]")
.add("super=" + super.toString())
.add("cost=" + this.cost)
.toString();
}
}