Skip to content
Open
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
12 changes: 2 additions & 10 deletions authme-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,8 @@
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>de.rtner</groupId>
<artifactId>PBKDF2</artifactId>
</dependency>
<dependency>
<groupId>de.mkammerer</groupId>
<artifactId>argon2-jvm-nolibs</artifactId>
</dependency>
<dependency>
<groupId>at.favre.lib</groupId>
<artifactId>bcrypt</artifactId>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>com.warrenstrange</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package fr.xephi.authme.security.crypts;

import de.rtner.security.auth.spi.PBKDF2Engine;
import de.rtner.security.auth.spi.PBKDF2Parameters;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.params.KeyParameter;

import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

public abstract class AbstractPbkdf2 extends HexSaltedMethod {

Expand All @@ -15,12 +19,13 @@ protected AbstractPbkdf2(Settings settings, int defaultRounds) {
}

protected byte[] deriveKey(String password, byte[] saltBytes, int iterations, int keyLength) {
PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", saltBytes, iterations);
return new PBKDF2Engine(params).deriveKey(password, keyLength);
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA256Digest());
gen.init(password.getBytes(StandardCharsets.UTF_8), saltBytes, iterations);
return ((KeyParameter) gen.generateDerivedMacParameters(keyLength * 8)).getKey();
}

protected boolean verifyKey(String password, byte[] saltBytes, int iterations, byte[] expectedKey) {
PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", saltBytes, iterations, expectedKey);
return new PBKDF2Engine(params).verifyKey(password);
byte[] computed = deriveKey(password, saltBytes, iterations, expectedKey.length);
return MessageDigest.isEqual(computed, expectedKey);
}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,96 @@
package fr.xephi.authme.security.crypts;

import de.mkammerer.argon2.Argon2Constants;
import de.mkammerer.argon2.Argon2Factory;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.crypts.description.HasSalt;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;

import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;

@Recommendation(Usage.RECOMMENDED)
@HasSalt(value = SaltType.TEXT, length = Argon2Constants.DEFAULT_SALT_LENGTH)
@HasSalt(value = SaltType.TEXT, length = 16)
// Note: Argon2 is actually a salted algorithm but salt generation is handled internally
// and isn't exposed to the outside, so we treat it as an unsalted implementation
public class Argon2 extends UnsaltedMethod {

private static ConsoleLogger logger = ConsoleLoggerFactory.get(Argon2.class);
private static final int ITERATIONS = 2;
private static final int MEMORY_KB = 65536;
private static final int PARALLELISM = 1;
private static final int SALT_BYTES = 16;
private static final int HASH_BYTES = 32;

private de.mkammerer.argon2.Argon2 argon2;
private final SecureRandom random = new SecureRandom();

public Argon2() {
argon2 = Argon2Factory.create();
@Override
public String computeHash(String password) {
byte[] salt = new byte[SALT_BYTES];
random.nextBytes(salt);
byte[] hash = derive(password.toCharArray(), salt, ITERATIONS, MEMORY_KB, PARALLELISM, HASH_BYTES);
Base64.Encoder enc = Base64.getEncoder().withoutPadding();
return "$argon2i$v=19$m=" + MEMORY_KB + ",t=" + ITERATIONS + ",p=" + PARALLELISM
+ "$" + enc.encodeToString(salt)
+ "$" + enc.encodeToString(hash);
}

/**
* Checks if the argon2 library is available in java.library.path.
*
* @return true if the library is present, false otherwise
*/
public static boolean isLibraryLoaded() {
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
String[] parts = hashedPassword.getHash().split("\\$");
// Expected: ["", "argon2i", "v=19", "m=65536,t=2,p=1", "<salt_b64>", "<hash_b64>"]
if (parts.length != 6 || !"argon2i".equals(parts[1])) {
return false;
}
try {
System.loadLibrary("argon2");
return true;
} catch (UnsatisfiedLinkError e) {
logger.logException(
"Cannot find argon2 library: https://github.com/AuthMe/AuthMeReloaded/wiki/Argon2-as-Password-Hash", e);
int[] params = parseParams(parts[3]); // m, t, p
byte[] salt = decodeNoPadding(parts[4]);
byte[] expected = decodeNoPadding(parts[5]);
byte[] computed = derive(password.toCharArray(), salt, params[1], params[0], params[2], expected.length);
return MessageDigest.isEqual(computed, expected);
} catch (IllegalArgumentException e) {
return false;
}
return false;
}

@Override
public String computeHash(String password) {
return argon2.hash(2, 65536, 1, password);
private static byte[] derive(char[] password, byte[] salt, int iterations, int memoryKb, int parallelism, int hashLen) {
Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i)
.withVersion(Argon2Parameters.ARGON2_VERSION_13)
.withIterations(iterations)
.withMemoryAsKB(memoryKb)
.withParallelism(parallelism)
.withSalt(salt)
.build();
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(params);
byte[] result = new byte[hashLen];
generator.generateBytes(password, result);
return result;
}

@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return argon2.verify(hashedPassword.getHash(), password);
/** Parses "m=65536,t=2,p=1" → [m, t, p]. */
private static int[] parseParams(String paramStr) {
int[] result = new int[3];
for (String kv : paramStr.split(",")) {
String[] pair = kv.split("=");
int v = Integer.parseInt(pair[1]);
switch (pair[0]) {
case "m": result[0] = v; break;
case "t": result[1] = v; break;
case "p": result[2] = v; break;
}
}
return result;
}

/** Decodes base64 without padding (PHC format omits '='). */
private static byte[] decodeNoPadding(String s) {
switch (s.length() % 4) {
case 2: s += "=="; break;
case 3: s += "="; break;
default: break;
}
return Base64.getDecoder().decode(s);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package fr.xephi.authme.security.crypts;

import at.favre.lib.crypto.bcrypt.BCrypt.Version;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.HooksSettings;

Expand All @@ -18,6 +17,6 @@ public BCrypt(Settings settings) {

private static BCryptHasher createHasher(Settings settings) {
int bCryptLog2Rounds = settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND);
return new BCryptHasher(Version.VERSION_2A, bCryptLog2Rounds);
return new BCryptHasher("2a", bCryptLog2Rounds);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package fr.xephi.authme.security.crypts;

import at.favre.lib.crypto.bcrypt.BCrypt;

import javax.inject.Inject;

/**
Expand All @@ -15,6 +13,6 @@ public BCrypt2y() {
}

public BCrypt2y(int cost) {
super(new BCryptHasher(BCrypt.Version.VERSION_2Y, cost));
super(new BCryptHasher("2y", cost));
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package fr.xephi.authme.security.crypts;

import at.favre.lib.crypto.bcrypt.BCrypt;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.util.RandomStringUtils;
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;

import static java.nio.charset.StandardCharsets.UTF_8;
import java.security.SecureRandom;
import java.util.Arrays;

/**
* Wraps a {@link BCrypt.Hasher} instance and provides methods suitable for use in AuthMe.
* Wraps BouncyCastle's {@link OpenBSDBCrypt} and provides methods suitable for use in AuthMe.
*/
public class BCryptHasher {

Expand All @@ -16,28 +16,29 @@ public class BCryptHasher {
/** Number of characters of the salt in its radix64-encoded form. */
public static final int SALT_LENGTH_ENCODED = 22;

private final BCrypt.Hasher hasher;
private final String version;
private final int costFactor;

/**
* Constructor.
*
* @param version the BCrypt version the instance should generate
* @param version the BCrypt version string ("2a" or "2y")
* @param costFactor the log2 cost factor to use
*/
public BCryptHasher(BCrypt.Version version, int costFactor) {
this.hasher = BCrypt.with(version);
public BCryptHasher(String version, int costFactor) {
this.version = version;
this.costFactor = costFactor;
}

public HashedPassword hash(String password) {
byte[] hash = hasher.hash(costFactor, password.getBytes(UTF_8));
return new HashedPassword(new String(hash, UTF_8));
byte[] salt = new byte[BYTES_IN_SALT];
new SecureRandom().nextBytes(salt);
String hash = OpenBSDBCrypt.generate(version, password.toCharArray(), salt, costFactor);
return new HashedPassword(hash);
}

public String hashWithRawSalt(String password, byte[] rawSalt) {
byte[] hash = hasher.hash(costFactor, rawSalt, password.getBytes(UTF_8));
return new String(hash, UTF_8);
return OpenBSDBCrypt.generate(version, password.toCharArray(), rawSalt, costFactor);
}

/**
Expand All @@ -48,31 +49,61 @@ public String hashWithRawSalt(String password, byte[] rawSalt) {
* @return true if the password matches the hash, false otherwise
*/
public static boolean comparePassword(String password, String hash) {
if (HashUtils.isValidBcryptHash(hash)) {
BCrypt.Result result = BCrypt.verifyer().verify(password.getBytes(UTF_8), hash.getBytes(UTF_8));
return result.verified;
if (fr.xephi.authme.security.HashUtils.isValidBcryptHash(hash)) {
return OpenBSDBCrypt.checkPassword(hash, password.toCharArray());
}
return false;
}

/**
* Generates a salt for usage in BCrypt. The returned salt is not yet encoded.
* <p>
* Internally, the BCrypt library in {@link BCrypt.Hasher#hash(int, byte[])} uses the following:
* {@code Bytes.random(16, secureRandom).encodeUtf8();}
* <p>
* Because our {@link EncryptionMethod} interface works with {@code String} types we need to make sure that the
* generated bytes in the salt are suitable for conversion into a String, such that calling String#getBytes will
* yield the same number of bytes again. Thus, we are forced to limit the range of characters we use. Ideally we'd
* only have to pass the salt in its encoded form so that we could make use of the entire "spectrum" of values,
* which proves difficult to achieve with the underlying BCrypt library. However, the salt needs to be generated
* manually only for testing purposes; production code should always hash passwords using
* {@link EncryptionMethod#computeHash(String, String)}, which internally may represent salts in more suitable
* formats.
*
* @return the salt for a BCrypt hash
*/
public static String generateSalt() {
return RandomStringUtils.generateLowerUpper(BYTES_IN_SALT);
}

// BCrypt modified-base64 alphabet (OpenBSD variant)
private static final byte[] B64_INDEX;
static {
B64_INDEX = new byte[128];
Arrays.fill(B64_INDEX, (byte) -1);
String table = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (int i = 0; i < table.length(); i++) {
B64_INDEX[table.charAt(i)] = (byte) i;
}
}

/**
* Decodes a BCrypt-modified-base64 encoded salt (22 chars) into raw bytes (16 bytes).
* The BCrypt alphabet differs from standard base64 and uses a slightly different character set.
*
* @param saltB64 the 22-character BCrypt-base64 encoded salt
* @return 16 raw salt bytes
*/
public static byte[] decodeSalt(String saltB64) {
byte[] out = new byte[BYTES_IN_SALT];
for (int i = 0, j = 0; i < saltB64.length() - 1 && j < BYTES_IN_SALT; ) {
int c0 = b64Char(saltB64, i++);
int c1 = b64Char(saltB64, i++);
out[j++] = (byte) ((c0 << 2) | (c1 >> 4));
if (j >= BYTES_IN_SALT || i >= saltB64.length()) break;
int c2 = b64Char(saltB64, i++);
out[j++] = (byte) (((c1 & 0x0f) << 4) | (c2 >> 2));
if (j >= BYTES_IN_SALT || i >= saltB64.length()) break;
int c3 = b64Char(saltB64, i++);
out[j++] = (byte) (((c2 & 0x03) << 6) | c3);
}
return out;
}

private static int b64Char(String s, int pos) {
char c = s.charAt(pos);
int v = (c < 128) ? B64_INDEX[c] : -1;
if (v == -1) {
throw new IllegalArgumentException("Invalid BCrypt base64 character '" + c + "'");
}
return v;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package fr.xephi.authme.security.crypts;

import at.favre.lib.crypto.bcrypt.BCrypt;
import at.favre.lib.crypto.bcrypt.IllegalBCryptFormatException;
import fr.xephi.authme.security.crypts.description.HasSalt;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
import fr.xephi.authme.util.RandomStringUtils;

import static java.nio.charset.StandardCharsets.UTF_8;


/**
* Implementation for Ipb4 (Invision Power Board 4).
Expand All @@ -22,21 +18,16 @@
@HasSalt(value = SaltType.TEXT, length = BCryptHasher.SALT_LENGTH_ENCODED)
public class Ipb4 implements EncryptionMethod {

private BCryptHasher bCryptHasher = new BCryptHasher(BCrypt.Version.VERSION_2A, 13);
private BCryptHasher bCryptHasher = new BCryptHasher("2a", 13);

@Override
public String computeHash(String password, String salt, String name) {
// Since the radix64-encoded salt is necessary to be stored separately as well, the incoming salt here is
// radix64-encoded (see #generateSalt()). This means we first need to decode it before passing into the
// bcrypt hasher... We cheat by inserting the encoded salt into a dummy bcrypt hash so that we can parse it
// with the BCrypt utilities.
// This method (with specific salt) is only used for testing purposes, so this approach should be OK.

String dummyHash = "$2a$10$" + salt + "3Cfb5GnwvKhJ20r.hMjmcNkIT9.Uh9K";
// The salt here is the 22-char BCrypt-modified-base64 encoded salt (see #generateSalt).
// This method (with specific salt) is only used for testing purposes.
try {
BCrypt.HashData parseResult = BCrypt.Version.VERSION_2A.parser.parse(dummyHash.getBytes(UTF_8));
return bCryptHasher.hashWithRawSalt(password, parseResult.rawSalt);
} catch (IllegalBCryptFormatException |IllegalArgumentException e) {
byte[] rawSalt = BCryptHasher.decodeSalt(salt);
return bCryptHasher.hashWithRawSalt(password, rawSalt);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Cannot parse hash with salt '" + salt + "'", e);
}
}
Expand All @@ -45,7 +36,7 @@ public String computeHash(String password, String salt, String name) {
public HashedPassword computeHash(String password, String name) {
HashedPassword hash = bCryptHasher.hash(password);

// 7 chars prefix, then 22 chars which is the encoded salt, which we need again
// 7 chars prefix ($2a$XX$), then 22 chars which is the encoded salt, which we need again
String salt = hash.getHash().substring(7, 29);
return new HashedPassword(hash.getHash(), salt);
}
Expand Down
Loading
Loading