Sync repo files to b22

This commit is contained in:
HaHaWTH
2023-09-20 02:15:22 +08:00
parent ed320196fe
commit 3f6a7ce48f
478 changed files with 94676 additions and 0 deletions
@@ -0,0 +1,59 @@
package fr.xephi.authme.security;
import fr.xephi.authme.security.crypts.EncryptionMethod;
/**
* Hash algorithms supported by AuthMe.
*/
public enum HashAlgorithm {
ARGON2(fr.xephi.authme.security.crypts.Argon2.class),
BCRYPT(fr.xephi.authme.security.crypts.BCrypt.class),
BCRYPT2Y(fr.xephi.authme.security.crypts.BCrypt2y.class),
CMW(fr.xephi.authme.security.crypts.CmwCrypt.class),
CRAZYCRYPT1(fr.xephi.authme.security.crypts.CrazyCrypt1.class),
IPB3(fr.xephi.authme.security.crypts.Ipb3.class),
IPB4(fr.xephi.authme.security.crypts.Ipb4.class),
JOOMLA(fr.xephi.authme.security.crypts.Joomla.class),
MD5VB(fr.xephi.authme.security.crypts.Md5vB.class),
MYBB(fr.xephi.authme.security.crypts.MyBB.class),
PBKDF2(fr.xephi.authme.security.crypts.Pbkdf2.class),
PBKDF2DJANGO(fr.xephi.authme.security.crypts.Pbkdf2Django.class),
PHPBB(fr.xephi.authme.security.crypts.PhpBB.class),
PHPFUSION(fr.xephi.authme.security.crypts.PhpFusion.class),
ROYALAUTH(fr.xephi.authme.security.crypts.RoyalAuth.class),
SALTED2MD5(fr.xephi.authme.security.crypts.Salted2Md5.class),
SALTEDSHA512(fr.xephi.authme.security.crypts.SaltedSha512.class),
SHA256(fr.xephi.authme.security.crypts.Sha256.class),
SMF(fr.xephi.authme.security.crypts.Smf.class),
TWO_FACTOR(fr.xephi.authme.security.crypts.TwoFactor.class),
WBB3(fr.xephi.authme.security.crypts.Wbb3.class),
WBB4(fr.xephi.authme.security.crypts.Wbb4.class),
WORDPRESS(fr.xephi.authme.security.crypts.Wordpress.class),
XAUTH(fr.xephi.authme.security.crypts.XAuth.class),
XFBCRYPT(fr.xephi.authme.security.crypts.XfBCrypt.class),
CUSTOM(null),
@Deprecated DOUBLEMD5(fr.xephi.authme.security.crypts.DoubleMd5.class),
@Deprecated MD5(fr.xephi.authme.security.crypts.Md5.class),
@Deprecated PLAINTEXT(null),
@Deprecated SHA1(fr.xephi.authme.security.crypts.Sha1.class),
@Deprecated SHA512(fr.xephi.authme.security.crypts.Sha512.class),
@Deprecated WHIRLPOOL(fr.xephi.authme.security.crypts.Whirlpool.class);
private final Class<? extends EncryptionMethod> clazz;
/**
* Constructor for HashAlgorithm.
*
* @param clazz The class of the hash implementation.
*/
HashAlgorithm(Class<? extends EncryptionMethod> clazz) {
this.clazz = clazz;
}
public Class<? extends EncryptionMethod> getClazz() {
return clazz;
}
}
@@ -0,0 +1,121 @@
package fr.xephi.authme.security;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Hashing utilities (interface for common hashing algorithms).
*/
public final class HashUtils {
private HashUtils() {
}
/**
* Generate the SHA-1 digest of the given message.
*
* @param message The message to hash
* @return The resulting SHA-1 digest
*/
public static String sha1(String message) {
return hash(message, MessageDigestAlgorithm.SHA1);
}
/**
* Generate the SHA-256 digest of the given message.
*
* @param message The message to hash
* @return The resulting SHA-256 digest
*/
public static String sha256(String message) {
return hash(message, MessageDigestAlgorithm.SHA256);
}
/**
* Generate the SHA-512 digest of the given message.
*
* @param message The message to hash
* @return The resulting SHA-512 digest
*/
public static String sha512(String message) {
return hash(message, MessageDigestAlgorithm.SHA512);
}
/**
* Generate the MD5 digest of the given message.
*
* @param message The message to hash
* @return The resulting MD5 digest
*/
public static String md5(String message) {
return hash(message, MessageDigestAlgorithm.MD5);
}
/**
* Return a {@link MessageDigest} instance for the given algorithm.
*
* @param algorithm The desired algorithm
* @return MessageDigest instance for the given algorithm
*/
public static MessageDigest getDigest(MessageDigestAlgorithm algorithm) {
try {
return MessageDigest.getInstance(algorithm.getKey());
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("Your system seems not to support the hash algorithm '"
+ algorithm.getKey() + "'");
}
}
/**
* Return whether the given hash starts like a BCrypt hash. Checking with this method
* beforehand prevents the BCryptHasher from throwing certain exceptions.
*
* @param hash The salt to verify
* @return True if the salt is valid, false otherwise
*/
public static boolean isValidBcryptHash(String hash) {
return hash.length() == 60 && hash.substring(0, 2).equals("$2");
}
/**
* Checks whether the two strings are equal to each other in a time-constant manner.
* This helps to avoid timing side channel attacks,
* cf. <a href="https://github.com/AuthMe/AuthMeReloaded/issues/1561">issue #1561</a>.
*
* @param string1 first string
* @param string2 second string
* @return true if the strings are equal to each other, false otherwise
*/
public static boolean isEqual(String string1, String string2) {
return MessageDigest.isEqual(
string1.getBytes(StandardCharsets.UTF_8), string2.getBytes(StandardCharsets.UTF_8));
}
/**
* Hash the message with the given algorithm and return the hash in its hexadecimal notation.
*
* @param message The message to hash
* @param algorithm The algorithm to hash the message with
* @return The digest in its hexadecimal representation
*/
public static String hash(String message, MessageDigest algorithm) {
algorithm.reset();
algorithm.update(message.getBytes());
byte[] digest = algorithm.digest();
return String.format("%0" + (digest.length << 1) + "x", new BigInteger(1, digest));
}
/**
* Hash the message with the given algorithm and return the hash in its hexadecimal notation.
*
* @param message The message to hash
* @param algorithm The algorithm to hash the message with
* @return The digest in its hexadecimal representation
*/
private static String hash(String message, MessageDigestAlgorithm algorithm) {
return hash(message, getDigest(algorithm));
}
}
@@ -0,0 +1,30 @@
package fr.xephi.authme.security;
import java.security.MessageDigest;
/**
* The Java-supported names to get a {@link MessageDigest} instance with.
*
* @see <a href="http://docs.oracle.com/javase/1.5.0/docs/guide/security/CryptoSpec.html#AppA">
* Crypto Spec Appendix A: Standard Names</a>
*/
public enum MessageDigestAlgorithm {
MD5("MD5"),
SHA1("SHA-1"),
SHA256("SHA-256"),
SHA512("SHA-512");
private final String key;
MessageDigestAlgorithm(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
@@ -0,0 +1,164 @@
package fr.xephi.authme.security;
import ch.jalu.injector.factory.Factory;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.PasswordEncryptionEvent;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.security.crypts.EncryptionMethod;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import org.bukkit.plugin.PluginManager;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.Collection;
import java.util.Locale;
/**
* Manager class for password-related operations.
*/
public class PasswordSecurity implements Reloadable {
@Inject
private Settings settings;
@Inject
private DataSource dataSource;
@Inject
private PluginManager pluginManager;
@Inject
private Factory<EncryptionMethod> encryptionMethodFactory;
private EncryptionMethod encryptionMethod;
private Collection<HashAlgorithm> legacyAlgorithms;
/**
* Load or reload the configuration.
*/
@PostConstruct
@Override
public void reload() {
HashAlgorithm algorithm = settings.getProperty(SecuritySettings.PASSWORD_HASH);
this.encryptionMethod = initializeEncryptionMethodWithEvent(algorithm);
this.legacyAlgorithms = settings.getProperty(SecuritySettings.LEGACY_HASHES);
}
/**
* Compute the hash of the configured algorithm for the given password and username.
*
* @param password The password to hash
* @param playerName The player's name
*
* @return The password hash
*/
public HashedPassword computeHash(String password, String playerName) {
String playerLowerCase = playerName.toLowerCase(Locale.ROOT);
return encryptionMethod.computeHash(password, playerLowerCase);
}
/**
* Check if the given password matches the player's stored password.
*
* @param password The password to check
* @param playerName The player to check for
*
* @return True if the password is correct, false otherwise
*/
public boolean comparePassword(String password, String playerName) {
HashedPassword auth = dataSource.getPassword(playerName);
return auth != null && comparePassword(password, auth, playerName);
}
/**
* Check if the given password matches the given hashed password.
*
* @param password The password to check
* @param hashedPassword The hashed password to check against
* @param playerName The player to check for
*
* @return True if the password matches, false otherwise
*/
public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) {
String playerLowerCase = playerName.toLowerCase(Locale.ROOT);
return methodMatches(encryptionMethod, password, hashedPassword, playerLowerCase)
|| compareWithLegacyHashes(password, hashedPassword, playerLowerCase);
}
/**
* Compare the given hash with the configured legacy encryption methods to support
* the migration to a new encryption method. Upon a successful match, the password
* will be hashed with the new encryption method and persisted.
*
* @param password The clear-text password to check
* @param hashedPassword The encrypted password to test the clear-text password against
* @param playerName The name of the player
*
* @return True if there was a password match with a configured legacy encryption method, false otherwise
*/
private boolean compareWithLegacyHashes(String password, HashedPassword hashedPassword, String playerName) {
for (HashAlgorithm algorithm : legacyAlgorithms) {
EncryptionMethod method = initializeEncryptionMethod(algorithm);
if (methodMatches(method, password, hashedPassword, playerName)) {
hashAndSavePasswordWithNewAlgorithm(password, playerName);
return true;
}
}
return false;
}
/**
* Verify with the given encryption method whether the password matches the hash after checking that
* the method can be called safely with the given data.
*
* @param method The encryption method to use
* @param password The password to check
* @param hashedPassword The hash to check against
* @param playerName The name of the player
*
* @return True if the password matched, false otherwise
*/
private static boolean methodMatches(EncryptionMethod method, String password,
HashedPassword hashedPassword, String playerName) {
return method != null && (!method.hasSeparateSalt() || hashedPassword.getSalt() != null)
&& method.comparePassword(password, hashedPassword, playerName);
}
/**
* Get the encryption method from the given {@link HashAlgorithm} value and emit a
* {@link PasswordEncryptionEvent}. The encryption method from the event is then returned,
* which may have been changed by an external listener.
*
* @param algorithm The algorithm to retrieve the encryption method for
*
* @return The encryption method
*/
private EncryptionMethod initializeEncryptionMethodWithEvent(HashAlgorithm algorithm) {
EncryptionMethod method = initializeEncryptionMethod(algorithm);
PasswordEncryptionEvent event = new PasswordEncryptionEvent(method);
pluginManager.callEvent(event);
return event.getMethod();
}
/**
* Initialize the encryption method associated with the given hash algorithm.
*
* @param algorithm The algorithm to retrieve the encryption method for
*
* @return The associated encryption method, or null if CUSTOM / deprecated
*/
private EncryptionMethod initializeEncryptionMethod(HashAlgorithm algorithm) {
if (HashAlgorithm.CUSTOM.equals(algorithm) || HashAlgorithm.PLAINTEXT.equals(algorithm)) {
return null;
}
return encryptionMethodFactory.newInstance(algorithm.getClazz());
}
private void hashAndSavePasswordWithNewAlgorithm(String password, String playerName) {
HashedPassword hashedPassword = encryptionMethod.computeHash(password, playerName);
dataSource.updatePassword(playerName, hashedPassword);
}
}
@@ -0,0 +1,51 @@
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;
@Recommendation(Usage.RECOMMENDED)
@HasSalt(value = SaltType.TEXT, length = Argon2Constants.DEFAULT_SALT_LENGTH)
// 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 de.mkammerer.argon2.Argon2 argon2;
public Argon2() {
argon2 = Argon2Factory.create();
}
/**
* Checks if the argon2 library is available in java.library.path.
*
* @return true if the library is present, false otherwise
*/
public static boolean isLibraryLoaded() {
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);
}
return false;
}
@Override
public String computeHash(String password) {
return argon2.hash(2, 65536, 1, password);
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return argon2.verify(hashedPassword.getHash(), password);
}
}
@@ -0,0 +1,23 @@
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;
import javax.inject.Inject;
/**
* BCrypt hash algorithm with configurable cost factor.
*/
public class BCrypt extends BCryptBasedHash {
@Inject
public BCrypt(Settings settings) {
super(createHasher(settings));
}
private static BCryptHasher createHasher(Settings settings) {
int bCryptLog2Rounds = settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND);
return new BCryptHasher(Version.VERSION_2A, bCryptLog2Rounds);
}
}
@@ -0,0 +1,20 @@
package fr.xephi.authme.security.crypts;
import at.favre.lib.crypto.bcrypt.BCrypt;
import javax.inject.Inject;
/**
* Hash for BCrypt in the $2y$ variant. Uses a fixed cost factor of 10.
*/
public class BCrypt2y extends BCryptBasedHash {
@Inject
public BCrypt2y() {
this(10);
}
public BCrypt2y(int cost) {
super(new BCryptHasher(BCrypt.Version.VERSION_2Y, cost));
}
}
@@ -0,0 +1,48 @@
package fr.xephi.authme.security.crypts;
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 static fr.xephi.authme.security.crypts.BCryptHasher.SALT_LENGTH_ENCODED;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Abstract parent for BCrypt-based hash algorithms.
*/
@Recommendation(Usage.RECOMMENDED)
@HasSalt(value = SaltType.TEXT, length = SALT_LENGTH_ENCODED)
public abstract class BCryptBasedHash implements EncryptionMethod {
private final BCryptHasher bCryptHasher;
public BCryptBasedHash(BCryptHasher bCryptHasher) {
this.bCryptHasher = bCryptHasher;
}
@Override
public HashedPassword computeHash(String password, String name) {
return bCryptHasher.hash(password);
}
@Override
public String computeHash(String password, String salt, String name) {
return bCryptHasher.hashWithRawSalt(password, salt.getBytes(UTF_8));
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return BCryptHasher.comparePassword(password, hashedPassword.getHash());
}
@Override
public String generateSalt() {
return BCryptHasher.generateSalt();
}
@Override
public boolean hasSeparateSalt() {
return false;
}
}
@@ -0,0 +1,78 @@
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 static java.nio.charset.StandardCharsets.UTF_8;
/**
* Wraps a {@link BCrypt.Hasher} instance and provides methods suitable for use in AuthMe.
*/
public class BCryptHasher {
/** Number of bytes in a BCrypt salt (not encoded). */
public static final int BYTES_IN_SALT = 16;
/** 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 int costFactor;
/**
* Constructor.
*
* @param version the BCrypt version the instance should generate
* @param costFactor the log2 cost factor to use
*/
public BCryptHasher(BCrypt.Version version, int costFactor) {
this.hasher = BCrypt.with(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));
}
public String hashWithRawSalt(String password, byte[] rawSalt) {
byte[] hash = hasher.hash(costFactor, rawSalt, password.getBytes(UTF_8));
return new String(hash, UTF_8);
}
/**
* Verifies that the given password is correct for the provided BCrypt hash.
*
* @param password the password to check with
* @param hash the hash to check against
* @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;
}
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);
}
}
@@ -0,0 +1,14 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
/**
* Hash algorithm to hook into the CMS <a href="http://craftmywebsite.fr/">Craft My Website</a>.
*/
public class CmwCrypt extends UnsaltedMethod {
@Override
public String computeHash(String password) {
return HashUtils.md5(HashUtils.sha1(password));
}
}
@@ -0,0 +1,31 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.MessageDigestAlgorithm;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class CrazyCrypt1 extends UsernameSaltMethod {
private static final char[] CRYPTCHARS =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private static String byteArrayToHexString(final byte... args) {
final char[] chars = new char[args.length * 2];
for (int i = 0; i < args.length; i++) {
chars[i * 2] = CRYPTCHARS[(args[i] >> 4) & 0xF];
chars[i * 2 + 1] = CRYPTCHARS[(args[i]) & 0xF];
}
return new String(chars);
}
@Override
public HashedPassword computeHash(String password, String name) {
final String text = "ÜÄaeut//&/=I " + password + "7421€547" + name + "__+IÄIH§%NK " + password;
final MessageDigest md = HashUtils.getDigest(MessageDigestAlgorithm.SHA512);
md.update(text.getBytes(StandardCharsets.UTF_8), 0, text.length());
return new HashedPassword(byteArrayToHexString(md.digest()));
}
}
@@ -0,0 +1,17 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.md5;
@Deprecated
@Recommendation(Usage.DEPRECATED)
public class DoubleMd5 extends UnsaltedMethod {
@Override
public String computeHash(String password) {
return md5(md5(password));
}
}
@@ -0,0 +1,61 @@
package fr.xephi.authme.security.crypts;
/**
* Public interface for custom password encryption methods.
* <p>
* Instantiation of these methods is done via automatic dependency injection.
*/
public interface EncryptionMethod {
/**
* Hash the given password for the given player name.
*
* @param password The password to hash
* @param name The name of the player (sometimes required to generate a salt with)
*
* @return The hash result for the password.
* @see HashedPassword
*/
HashedPassword computeHash(String password, String name);
/**
* Hash the given password with the given salt for the given player.
*
* @param password The password to hash
* @param salt The salt to add to the hash
* @param name The player's name (sometimes required to generate a salt with)
*
* @return The hashed password
* @see #hasSeparateSalt()
*/
String computeHash(String password, String salt, String name);
/**
* Check whether the given hash matches the clear-text password.
*
* @param password The clear-text password to verify
* @param hashedPassword The hash to check the password against
* @param name The player name to do the check for (sometimes required for generating the salt)
*
* @return True if the password matches, false otherwise
*/
boolean comparePassword(String password, HashedPassword hashedPassword, String name);
/**
* Generate a new salt to hash a password with.
*
* @return The generated salt, null if the method does not use a random text-based salt
*/
String generateSalt();
/**
* Return whether the encryption method requires the salt to be stored separately and
* passed again to {@link #comparePassword(String, HashedPassword, String)}. Note that
* an encryption method returning {@code false} does not imply that it uses no salt; it
* may be embedded into the hash or it may use the username as salt.
*
* @return True if the salt has to be stored and retrieved separately, false otherwise
*/
boolean hasSeparateSalt();
}
@@ -0,0 +1,48 @@
package fr.xephi.authme.security.crypts;
/**
* The result of a hash computation. See {@link #salt} for details.
*/
public class HashedPassword {
/** The generated hash. */
private final String hash;
/**
* The generated salt; may be null if no salt is used or if the salt is included
* in the hash output. The salt is only not null if {@link EncryptionMethod#hasSeparateSalt()}
* returns true for the associated encryption method.
* <p>
* When the field is not null, it must be stored into the salt column of the data source
* and retrieved again to compare a password with the hash.
*/
private final String salt;
/**
* Constructor.
*
* @param hash The computed hash
* @param salt The generated salt
*/
public HashedPassword(String hash, String salt) {
this.hash = hash;
this.salt = salt;
}
/**
* Constructor for a hash with no separate salt.
*
* @param hash The computed hash
*/
public HashedPassword(String hash) {
this(hash, null);
}
public String getHash() {
return hash;
}
public String getSalt() {
return salt;
}
}
@@ -0,0 +1,40 @@
package fr.xephi.authme.security.crypts;
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;
/**
* Common type for encryption methods which use a random String of hexadecimal characters
* and store the salt with the hash itself.
*/
@Recommendation(Usage.ACCEPTABLE)
@HasSalt(SaltType.TEXT) // See getSaltLength() for length
public abstract class HexSaltedMethod implements EncryptionMethod {
public abstract int getSaltLength();
@Override
public abstract String computeHash(String password, String salt, String name);
@Override
public HashedPassword computeHash(String password, String name) {
String salt = generateSalt();
return new HashedPassword(computeHash(password, salt, null));
}
@Override
public abstract boolean comparePassword(String password, HashedPassword hashedPassword, String name);
@Override
public String generateSalt() {
return RandomStringUtils.generateHex(getSaltLength());
}
@Override
public boolean hasSeparateSalt() {
return false;
}
}
@@ -0,0 +1,25 @@
package fr.xephi.authme.security.crypts;
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 fr.xephi.authme.security.HashUtils.md5;
@Recommendation(Usage.ACCEPTABLE)
@HasSalt(value = SaltType.TEXT, length = 5)
public class Ipb3 extends SeparateSaltMethod {
@Override
public String computeHash(String password, String salt, String name) {
return md5(md5(salt) + md5(password));
}
@Override
public String generateSalt() {
return RandomStringUtils.generateHex(5);
}
}
@@ -0,0 +1,67 @@
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).
* <p>
* The hash uses standard BCrypt with 13 as log<sub>2</sub> number of rounds. Additionally,
* Ipb4 requires that the salt be stored in the column "members_pass_hash" as well
* (even though BCrypt hashes already contain the salt within themselves).
*/
@Recommendation(Usage.DOES_NOT_WORK)
@HasSalt(value = SaltType.TEXT, length = BCryptHasher.SALT_LENGTH_ENCODED)
public class Ipb4 implements EncryptionMethod {
private BCryptHasher bCryptHasher = new BCryptHasher(BCrypt.Version.VERSION_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";
try {
BCrypt.HashData parseResult = BCrypt.Version.VERSION_2A.parser.parse(dummyHash.getBytes(UTF_8));
return bCryptHasher.hashWithRawSalt(password, parseResult.rawSalt);
} catch (IllegalBCryptFormatException |IllegalArgumentException e) {
throw new IllegalStateException("Cannot parse hash with salt '" + salt + "'", e);
}
}
@Override
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
String salt = hash.getHash().substring(7, 29);
return new HashedPassword(hash.getHash(), salt);
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return BCryptHasher.comparePassword(password, hashedPassword.getHash());
}
@Override
public String generateSalt() {
return RandomStringUtils.generateLowerUpper(22);
}
@Override
public boolean hasSeparateSalt() {
return true;
}
}
@@ -0,0 +1,29 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
@Recommendation(Usage.ACCEPTABLE)
public class Joomla extends HexSaltedMethod {
@Override
public String computeHash(String password, String salt, String name) {
return HashUtils.md5(password + salt) + ":" + salt;
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String unusedName) {
String hash = hashedPassword.getHash();
String[] hashParts = hash.split(":");
return hashParts.length == 2 && isEqual(hash, computeHash(password, hashParts[1], null));
}
@Override
public int getSaltLength() {
return 32;
}
}
@@ -0,0 +1,16 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
@Deprecated
@Recommendation(Usage.DEPRECATED)
public class Md5 extends UnsaltedMethod {
@Override
public String computeHash(String password) {
return HashUtils.md5(password);
}
}
@@ -0,0 +1,25 @@
package fr.xephi.authme.security.crypts;
import static fr.xephi.authme.security.HashUtils.isEqual;
import static fr.xephi.authme.security.HashUtils.md5;
public class Md5vB extends HexSaltedMethod {
@Override
public String computeHash(String password, String salt, String name) {
return "$MD5vb$" + salt + "$" + md5(md5(password) + salt);
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
String hash = hashedPassword.getHash();
String[] line = hash.split("\\$");
return line.length == 4 && isEqual(hash, computeHash(password, line[2], name));
}
@Override
public int getSaltLength() {
return 16;
}
}
@@ -0,0 +1,26 @@
package fr.xephi.authme.security.crypts;
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 fr.xephi.authme.security.HashUtils.md5;
@Recommendation(Usage.ACCEPTABLE)
@HasSalt(value = SaltType.TEXT, length = 8)
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"})
public class MyBB extends SeparateSaltMethod {
@Override
public String computeHash(String password, String salt, String name) {
return md5(md5(salt) + md5(password));
}
@Override
public String generateSalt() {
return RandomStringUtils.generateLowerUpper(8);
}
}
@@ -0,0 +1,62 @@
package fr.xephi.authme.security.crypts;
import com.google.common.primitives.Ints;
import de.rtner.misc.BinTools;
import de.rtner.security.auth.spi.PBKDF2Engine;
import de.rtner.security.auth.spi.PBKDF2Parameters;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import javax.inject.Inject;
@Recommendation(Usage.RECOMMENDED)
public class Pbkdf2 extends HexSaltedMethod {
private static final int DEFAULT_ROUNDS = 10_000;
private final ConsoleLogger logger = ConsoleLoggerFactory.get(Pbkdf2.class);
private int numberOfRounds;
@Inject
Pbkdf2(Settings settings) {
int configuredRounds = settings.getProperty(SecuritySettings.PBKDF2_NUMBER_OF_ROUNDS);
this.numberOfRounds = configuredRounds > 0 ? configuredRounds : DEFAULT_ROUNDS;
}
@Override
public String computeHash(String password, String salt, String name) {
String result = "pbkdf2_sha256$" + numberOfRounds + "$" + salt + "$";
PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), numberOfRounds);
PBKDF2Engine engine = new PBKDF2Engine(params);
return result + BinTools.bin2hex(engine.deriveKey(password, 64));
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String unusedName) {
String[] line = hashedPassword.getHash().split("\\$");
if (line.length != 4) {
return false;
}
Integer iterations = Ints.tryParse(line[1]);
if (iterations == null) {
logger.warning("Cannot read number of rounds for Pbkdf2: '" + line[1] + "'");
return false;
}
String salt = line[2];
byte[] derivedKey = BinTools.hex2bin(line[3]);
PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), iterations, derivedKey);
PBKDF2Engine engine = new PBKDF2Engine(params);
return engine.verifyKey(password);
}
@Override
public int getSaltLength() {
return 16;
}
}
@@ -0,0 +1,51 @@
package fr.xephi.authme.security.crypts;
import com.google.common.primitives.Ints;
import de.rtner.security.auth.spi.PBKDF2Engine;
import de.rtner.security.auth.spi.PBKDF2Parameters;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.crypts.description.AsciiRestricted;
import java.util.Base64;
@AsciiRestricted
public class Pbkdf2Django extends HexSaltedMethod {
private static final int DEFAULT_ITERATIONS = 24000;
private final ConsoleLogger logger = ConsoleLoggerFactory.get(Pbkdf2Django.class);
@Override
public String computeHash(String password, String salt, String name) {
String result = "pbkdf2_sha256$" + DEFAULT_ITERATIONS + "$" + salt + "$";
PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "ASCII", salt.getBytes(), DEFAULT_ITERATIONS);
PBKDF2Engine engine = new PBKDF2Engine(params);
return result + Base64.getEncoder().encodeToString(engine.deriveKey(password, 32));
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String unusedName) {
String[] line = hashedPassword.getHash().split("\\$");
if (line.length != 4) {
return false;
}
Integer iterations = Ints.tryParse(line[1]);
if (iterations == null) {
logger.warning("Cannot read number of rounds for Pbkdf2Django: '" + line[1] + "'");
return false;
}
String salt = line[2];
byte[] derivedKey = Base64.getDecoder().decode(line[3]);
PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "ASCII", salt.getBytes(), iterations, derivedKey);
PBKDF2Engine engine = new PBKDF2Engine(params);
return engine.verifyKey(password);
}
@Override
public int getSaltLength() {
return 12;
}
}
@@ -0,0 +1,162 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.MessageDigestAlgorithm;
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 java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import static fr.xephi.authme.security.HashUtils.isEqual;
import static fr.xephi.authme.security.crypts.BCryptHasher.SALT_LENGTH_ENCODED;
/**
* Encryption method compatible with phpBB3.
* <p>
* As tested with phpBB 3.2.1, by default new passwords are encrypted with BCrypt $2y$.
* For backwards compatibility, phpBB3 supports other hashes for comparison. This implementation
* successfully checks against phpBB's salted MD5 hashing algorithm (adaptation of phpass),
* as well as plain MD5.
*/
@Recommendation(Usage.ACCEPTABLE)
@HasSalt(value = SaltType.TEXT, length = SALT_LENGTH_ENCODED)
public class PhpBB implements EncryptionMethod {
private final BCrypt2y bCrypt2y = new BCrypt2y();
@Override
public HashedPassword computeHash(String password, String name) {
return bCrypt2y.computeHash(password, name);
}
@Override
public String computeHash(String password, String salt, String name) {
return bCrypt2y.computeHash(password, salt, name);
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
final String hash = hashedPassword.getHash();
if (HashUtils.isValidBcryptHash(hash)) {
return bCrypt2y.comparePassword(password, hashedPassword, name);
} else if (hash.length() == 34) {
return PhpassSaltedMd5.phpbb_check_hash(password, hash);
} else {
return isEqual(hash, PhpassSaltedMd5.md5(password));
}
}
@Override
public String generateSalt() {
// Salt length 22, as seen in https://github.com/phpbb/phpbb/blob/master/phpBB/phpbb/passwords/driver/bcrypt.php
// Ours generates 16 chars because the salt must not yet be encoded.
return BCryptHasher.generateSalt();
}
@Override
public boolean hasSeparateSalt() {
return false;
}
/**
* Java implementation of the salted MD5 as used in phpBB (adapted from phpass).
*
* @see <a href="https://github.com/phpbb/phpbb/blob/master/phpBB/phpbb/passwords/driver/salted_md5.php">phpBB's salted_md5.php</a>
* @see <a href="http://www.openwall.com/phpass/">phpass</a>
*/
private static final class PhpassSaltedMd5 {
private static final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static String md5(String data) {
try {
byte[] bytes = data.getBytes("ISO-8859-1");
MessageDigest md5er = HashUtils.getDigest(MessageDigestAlgorithm.MD5);
byte[] hash = md5er.digest(bytes);
return bytes2hex(hash);
} catch (UnsupportedEncodingException e) {
throw new UnsupportedOperationException(e);
}
}
private static int hexToInt(char ch) {
if (ch >= '0' && ch <= '9')
return ch - '0';
ch = Character.toUpperCase(ch);
if (ch >= 'A' && ch <= 'F')
return ch - 'A' + 0xA;
throw new IllegalArgumentException("Not a hex character: " + ch);
}
private static String bytes2hex(byte[] bytes) {
StringBuilder r = new StringBuilder(32);
for (byte b : bytes) {
String x = Integer.toHexString(b & 0xff);
if (x.length() < 2)
r.append('0');
r.append(x);
}
return r.toString();
}
private static String pack(String hex) {
StringBuilder buf = new StringBuilder();
for (int i = 0; i < hex.length(); i += 2) {
char c1 = hex.charAt(i);
char c2 = hex.charAt(i + 1);
char packed = (char) (hexToInt(c1) * 16 + hexToInt(c2));
buf.append(packed);
}
return buf.toString();
}
private static String _hash_encode64(String input, int count) {
StringBuilder output = new StringBuilder();
int i = 0;
do {
int value = input.charAt(i++);
output.append(itoa64.charAt(value & 0x3f));
if (i < count)
value |= input.charAt(i) << 8;
output.append(itoa64.charAt((value >> 6) & 0x3f));
if (i++ >= count)
break;
if (i < count)
value |= input.charAt(i) << 16;
output.append(itoa64.charAt((value >> 12) & 0x3f));
if (i++ >= count)
break;
output.append(itoa64.charAt((value >> 18) & 0x3f));
} while (i < count);
return output.toString();
}
private static String _hash_crypt_private(String password, String setting) {
String output = "*";
if (!setting.substring(0, 3).equals("$H$"))
return output;
int count_log2 = itoa64.indexOf(setting.charAt(3));
if (count_log2 < 7 || count_log2 > 30)
return output;
int count = 1 << count_log2;
String salt = setting.substring(4, 12);
if (salt.length() != 8)
return output;
String m1 = md5(salt + password);
String hash = pack(m1);
do {
hash = pack(md5(hash + password));
} while (--count > 0);
output = setting.substring(0, 12);
output += _hash_encode64(hash, 16);
return output;
}
private static boolean phpbb_check_hash(String password, String hash) {
return isEqual(hash, _hash_crypt_private(password, hash)); // #1561: fix timing issue
}
}
}
@@ -0,0 +1,48 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.crypts.description.AsciiRestricted;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import fr.xephi.authme.util.RandomStringUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@Recommendation(Usage.DO_NOT_USE)
@AsciiRestricted
public class PhpFusion extends SeparateSaltMethod {
@Override
public String computeHash(String password, String salt, String name) {
String algo = "HmacSHA256";
String keyString = HashUtils.sha1(salt);
try {
SecretKeySpec key = new SecretKeySpec(keyString.getBytes("UTF-8"), algo);
Mac mac = Mac.getInstance(algo);
mac.init(key);
byte[] bytes = mac.doFinal(password.getBytes("ASCII"));
StringBuilder hash = new StringBuilder();
for (byte aByte : bytes) {
String hex = Integer.toHexString(0xFF & aByte);
if (hex.length() == 1) {
hash.append('0');
}
hash.append(hex);
}
return hash.toString();
} catch (UnsupportedEncodingException | InvalidKeyException | NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("Cannot create PHPFUSION hash for " + name, e);
}
}
@Override
public String generateSalt() {
return RandomStringUtils.generateHex(12);
}
}
@@ -0,0 +1,19 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.MessageDigestAlgorithm;
import java.security.MessageDigest;
public class RoyalAuth extends UnsaltedMethod {
@Override
public String computeHash(String password) {
MessageDigest algorithm = HashUtils.getDigest(MessageDigestAlgorithm.SHA512);
for (int i = 0; i < 25; ++i) {
password = HashUtils.hash(password, algorithm);
}
return password;
}
}
@@ -0,0 +1,37 @@
package fr.xephi.authme.security.crypts;
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.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.RandomStringUtils;
import javax.inject.Inject;
import static fr.xephi.authme.security.HashUtils.md5;
@Recommendation(Usage.ACCEPTABLE) // presuming that length is something sensible (>= 8)
@HasSalt(value = SaltType.TEXT) // length defined by the doubleMd5SaltLength setting
public class Salted2Md5 extends SeparateSaltMethod {
private final int saltLength;
@Inject
public Salted2Md5(Settings settings) {
saltLength = settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH);
}
@Override
public String computeHash(String password, String salt, String name) {
return md5(md5(password) + salt);
}
@Override
public String generateSalt() {
return RandomStringUtils.generateHex(saltLength);
}
}
@@ -0,0 +1,20 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import fr.xephi.authme.util.RandomStringUtils;
@Recommendation(Usage.RECOMMENDED)
public class SaltedSha512 extends SeparateSaltMethod {
@Override
public String computeHash(String password, String salt, String name) {
return HashUtils.sha512(password + salt);
}
@Override
public String generateSalt() {
return RandomStringUtils.generateHex(32);
}
}
@@ -0,0 +1,32 @@
package fr.xephi.authme.security.crypts;
import static fr.xephi.authme.security.HashUtils.isEqual;
/**
* Common supertype for encryption methods which store their salt separately from the hash.
*/
public abstract class SeparateSaltMethod implements EncryptionMethod {
@Override
public abstract String computeHash(String password, String salt, String name);
@Override
public HashedPassword computeHash(String password, String name) {
String salt = generateSalt();
return new HashedPassword(computeHash(password, salt, name), salt);
}
@Override
public abstract String generateSalt();
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return isEqual(hashedPassword.getHash(), computeHash(password, hashedPassword.getSalt(), null));
}
@Override
public boolean hasSeparateSalt() {
return true;
}
}
@@ -0,0 +1,16 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
@Deprecated
@Recommendation(Usage.DEPRECATED)
public class Sha1 extends UnsaltedMethod {
@Override
public String computeHash(String password) {
return HashUtils.sha1(password);
}
}
@@ -0,0 +1,29 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
import static fr.xephi.authme.security.HashUtils.sha256;
@Recommendation(Usage.RECOMMENDED)
public class Sha256 extends HexSaltedMethod {
@Override
public String computeHash(String password, String salt, String name) {
return "$SHA$" + salt + "$" + sha256(sha256(password) + salt);
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
String hash = hashedPassword.getHash();
String[] line = hash.split("\\$");
return line.length == 4 && isEqual(hash, computeHash(password, line[2], name));
}
@Override
public int getSaltLength() {
return 16;
}
}
@@ -0,0 +1,16 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
@Deprecated
@Recommendation(Usage.DEPRECATED)
public class Sha512 extends UnsaltedMethod {
@Override
public String computeHash(String password) {
return HashUtils.sha512(password);
}
}
@@ -0,0 +1,51 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
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 java.util.Locale;
import static fr.xephi.authme.security.HashUtils.isEqual;
/**
* Hashing algorithm for SMF forums.
* <p>
* The hash algorithm is {@code sha1(strtolower($username) . $password)}. However, an additional four-character
* salt is generated for each user, used to generate the login cookie. Therefore, this implementation generates a salt
* and declares that it requires a separate salt (the SMF members table has a not-null constraint on the salt column).
*
* @see <a href="https://www.simplemachines.org/">Simple Machines Forum</a>
*/
@Recommendation(Usage.DO_NOT_USE)
@HasSalt(SaltType.USERNAME)
public class Smf implements EncryptionMethod {
@Override
public HashedPassword computeHash(String password, String name) {
return new HashedPassword(computeHash(password, null, name), generateSalt());
}
@Override
public String computeHash(String password, String salt, String name) {
return HashUtils.sha1(name.toLowerCase(Locale.ROOT) + password);
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return isEqual(hashedPassword.getHash(), computeHash(password, null, name));
}
@Override
public String generateSalt() {
return RandomStringUtils.generate(4);
}
@Override
public boolean hasSeparateSalt() {
return true;
}
}
@@ -0,0 +1,140 @@
package fr.xephi.authme.security.crypts;
import com.google.common.escape.Escaper;
import com.google.common.io.BaseEncoding;
import com.google.common.net.UrlEscapers;
import com.google.common.primitives.Ints;
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 javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Calendar;
import java.util.concurrent.TimeUnit;
/**
* Two factor authentication.
*
* @see <a href="http://thegreyblog.blogspot.com/2011/12/google-authenticator-using-it-in-your.html">Original source</a>
*/
@Recommendation(Usage.DOES_NOT_WORK)
@HasSalt(SaltType.NONE)
public class TwoFactor extends UnsaltedMethod {
private static final int SCRET_BYTE = 10;
private static final int SCRATCH_CODES = 5;
private static final int BYTES_PER_SCRATCH_CODE = 4;
private static final int TIME_PRECISION = 3;
private static final String CRYPTO_ALGO = "HmacSHA1";
private final ConsoleLogger logger = ConsoleLoggerFactory.get(TwoFactor.class);
/**
* Creates a link to a QR barcode with the provided secret.
*
* @param user the player's name
* @param host the server host
* @param secret the TOTP secret
* @return URL leading to a QR code
*/
public static String getQrBarcodeUrl(String user, String host, String secret) {
String format = "https://www.google.com/chart?chs=130x130&chld=M%%7C0&cht=qr&chl="
+ "otpauth://totp/"
+ "%s@%s%%3Fsecret%%3D%s";
Escaper urlEscaper = UrlEscapers.urlFragmentEscaper();
return String.format(format, urlEscaper.escape(user), urlEscaper.escape(host), secret);
}
@Override
public String computeHash(String password) {
// Allocating the buffer
byte[] buffer = new byte[SCRET_BYTE + SCRATCH_CODES * BYTES_PER_SCRATCH_CODE];
// Filling the buffer with random numbers.
// Notice: you want to reuse the same random generator
// while generating larger random number sequences.
new SecureRandom().nextBytes(buffer);
// Getting the key and converting it to Base32
byte[] secretKey = Arrays.copyOf(buffer, SCRET_BYTE);
return BaseEncoding.base32().encode(secretKey);
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
try {
return checkPassword(hashedPassword.getHash(), password);
} catch (Exception e) {
logger.logException("Failed to verify two auth code:", e);
return false;
}
}
private boolean checkPassword(String secretKey, String userInput)
throws NoSuchAlgorithmException, InvalidKeyException {
Integer code = Ints.tryParse(userInput);
if (code == null) {
//code is not an integer
return false;
}
long currentTime = Calendar.getInstance().getTimeInMillis() / TimeUnit.SECONDS.toMillis(30);
return checkCode(secretKey, code, currentTime);
}
private boolean checkCode(String secret, long code, long t) throws NoSuchAlgorithmException, InvalidKeyException {
byte[] decodedKey = BaseEncoding.base32().decode(secret);
// Window is used to check codes generated in the near past.
// You can use this value to tune how far you're willing to go.
int window = TIME_PRECISION;
for (int i = -window; i <= window; ++i) {
long hash = verifyCode(decodedKey, t + i);
if (hash == code) {
return true;
}
}
// The validation code is invalid.
return false;
}
private int verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
byte[] data = new byte[8];
long value = t;
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}
SecretKeySpec signKey = new SecretKeySpec(key, CRYPTO_ALGO);
Mac mac = Mac.getInstance(CRYPTO_ALGO);
mac.init(signKey);
byte[] hash = mac.doFinal(data);
int offset = hash[20 - 1] & 0xF;
// We're using a long because Java hasn't got unsigned int.
long truncatedHash = 0;
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
// We are dealing with signed bytes:
// we just keep the first byte.
truncatedHash |= (hash[offset + i] & 0xFF);
}
truncatedHash &= 0x7FFF_FFFF;
truncatedHash %= 1_000_000;
return (int) truncatedHash;
}
}
@@ -0,0 +1,43 @@
package fr.xephi.authme.security.crypts;
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 static fr.xephi.authme.security.HashUtils.isEqual;
/**
* Common type for encryption methods which do not use any salt whatsoever.
*/
@Recommendation(Usage.DO_NOT_USE)
@HasSalt(SaltType.NONE)
public abstract class UnsaltedMethod implements EncryptionMethod {
public abstract String computeHash(String password);
@Override
public HashedPassword computeHash(String password, String name) {
return new HashedPassword(computeHash(password));
}
@Override
public String computeHash(String password, String salt, String name) {
return computeHash(password);
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return isEqual(hashedPassword.getHash(), computeHash(password));
}
@Override
public String generateSalt() {
return null;
}
@Override
public boolean hasSeparateSalt() {
return false;
}
}
@@ -0,0 +1,41 @@
package fr.xephi.authme.security.crypts;
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 static fr.xephi.authme.security.HashUtils.isEqual;
/**
* Common supertype of encryption methods that use a player's username
* (or something based on it) as embedded salt.
*/
@Recommendation(Usage.DO_NOT_USE)
@HasSalt(SaltType.USERNAME)
public abstract class UsernameSaltMethod implements EncryptionMethod {
@Override
public abstract HashedPassword computeHash(String password, String name);
@Override
public String computeHash(String password, String salt, String name) {
return computeHash(password, name).getHash();
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
return isEqual(hashedPassword.getHash(), computeHash(password, name).getHash());
}
@Override
public String generateSalt() {
return null;
}
@Override
public boolean hasSeparateSalt() {
return false;
}
}
@@ -0,0 +1,25 @@
package fr.xephi.authme.security.crypts;
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 fr.xephi.authme.security.HashUtils.sha1;
@Recommendation(Usage.ACCEPTABLE)
@HasSalt(value = SaltType.TEXT, length = 40)
public class Wbb3 extends SeparateSaltMethod {
@Override
public String computeHash(String password, String salt, String name) {
return sha1(salt.concat(sha1(salt.concat(sha1(password)))));
}
@Override
public String generateSalt() {
return RandomStringUtils.generateHex(40);
}
}
@@ -0,0 +1,74 @@
package fr.xephi.authme.security.crypts;
import at.favre.lib.crypto.bcrypt.BCrypt;
import at.favre.lib.crypto.bcrypt.IllegalBCryptFormatException;
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 java.security.SecureRandom;
import static fr.xephi.authme.security.HashUtils.isEqual;
import static fr.xephi.authme.security.crypts.BCryptHasher.BYTES_IN_SALT;
import static fr.xephi.authme.security.crypts.BCryptHasher.SALT_LENGTH_ENCODED;
import static java.nio.charset.StandardCharsets.UTF_8;
@Recommendation(Usage.RECOMMENDED)
@HasSalt(value = SaltType.TEXT, length = SALT_LENGTH_ENCODED)
public class Wbb4 implements EncryptionMethod {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(Wbb4.class);
private BCryptHasher bCryptHasher = new BCryptHasher(BCrypt.Version.VERSION_2A, 8);
private SecureRandom random = new SecureRandom();
@Override
public HashedPassword computeHash(String password, String name) {
byte[] salt = new byte[BYTES_IN_SALT];
random.nextBytes(salt);
String hash = hashInternal(password, salt);
return new HashedPassword(hash);
}
@Override
public String computeHash(String password, String salt, String name) {
return hashInternal(password, salt.getBytes(UTF_8));
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
try {
BCrypt.HashData hashData = BCrypt.Version.VERSION_2A.parser.parse(hashedPassword.getHash().getBytes(UTF_8));
byte[] salt = hashData.rawSalt;
String computedHash = hashInternal(password, salt);
return isEqual(hashedPassword.getHash(), computedHash);
} catch (IllegalBCryptFormatException | IllegalArgumentException e) {
logger.logException("Invalid WBB4 hash:", e);
}
return false;
}
/**
* Hashes the given password with the provided salt twice: hash(hash(password, salt), salt).
*
* @param password the password to hash
* @param rawSalt the salt to use
* @return WBB4-compatible hash
*/
private String hashInternal(String password, byte[] rawSalt) {
return bCryptHasher.hashWithRawSalt(bCryptHasher.hashWithRawSalt(password, rawSalt), rawSalt);
}
@Override
public String generateSalt() {
return BCryptHasher.generateSalt();
}
@Override
public boolean hasSeparateSalt() {
return false;
}
}
@@ -0,0 +1,399 @@
package fr.xephi.authme.security.crypts;
/**
* The Whirlpool hashing function.
* <p/>
* <p/>
* <b>References</b>
* <p/>
* <p/>
* The Whirlpool algorithm was developed by <a
* href="mailto:pbarreto@scopus.com.br">Paulo S. L. M. Barreto</a> and <a
* href="mailto:vincent.rijmen@cryptomathic.com">Vincent Rijmen</a>.
* <p/>
* See P.S.L.M. Barreto, V. Rijmen, ``The Whirlpool hashing function,'' First
* NESSIE workshop, 2000 (tweaked version, 2003),
* <https://www.cosic.esat.kuleuven
* .ac.be/nessie/workshop/submissions/whirlpool.zip>
*
* @author Paulo S.L.M. Barreto
* @author Vincent Rijmen.
* @version 3.0 (2003.03.12)
* <p/>
* ====================================================================
* =========
* <p/>
* Differences from version 2.1:
* <p/>
* - Suboptimal diffusion matrix replaced by cir(1, 1, 4, 1, 8, 5, 2,
* 9).
* <p/>
* ====================================================================
* =========
* <p/>
* Differences from version 2.0:
* <p/>
* - Generation of ISO/IEC 10118-3 test vectors. - Bug fix: nonzero
* carry was ignored when tallying the data length (this bug apparently
* only manifested itself when feeding data in pieces rather than in a
* single chunk at once).
* <p/>
* Differences from version 1.0:
* <p/>
* - Original S-box replaced by the tweaked, hardware-efficient
* version.
* <p/>
* ====================================================================
* =========
* <p/>
* THIS SOFTWARE IS PROVIDED BY THE AUTHORS ''AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import java.util.Arrays;
@Deprecated
@Recommendation(Usage.DEPRECATED)
public class Whirlpool extends UnsaltedMethod {
/**
* The message digest size (in bits)
*/
public static final int DIGESTBITS = 512;
/**
* The message digest size (in bytes)
*/
public static final int DIGESTBYTES = DIGESTBITS >>> 3;
/**
* The number of rounds of the internal dedicated block cipher.
*/
protected static final int R = 10;
/**
* The substitution box.
*/
private static final String sbox = "\u1823\uc6E8\u87B8\u014F\u36A6\ud2F5\u796F\u9152" + "\u60Bc\u9B8E\uA30c\u7B35\u1dE0\ud7c2\u2E4B\uFE57" + "\u1577\u37E5\u9FF0\u4AdA\u58c9\u290A\uB1A0\u6B85" + "\uBd5d\u10F4\ucB3E\u0567\uE427\u418B\uA77d\u95d8" + "\uFBEE\u7c66\udd17\u479E\ucA2d\uBF07\uAd5A\u8333" + "\u6302\uAA71\uc819\u49d9\uF2E3\u5B88\u9A26\u32B0" + "\uE90F\ud580\uBEcd\u3448\uFF7A\u905F\u2068\u1AAE" + "\uB454\u9322\u64F1\u7312\u4008\uc3Ec\udBA1\u8d3d" + "\u9700\ucF2B\u7682\ud61B\uB5AF\u6A50\u45F3\u30EF" + "\u3F55\uA2EA\u65BA\u2Fc0\udE1c\uFd4d\u9275\u068A" + "\uB2E6\u0E1F\u62d4\uA896\uF9c5\u2559\u8472\u394c" + "\u5E78\u388c\ud1A5\uE261\uB321\u9c1E\u43c7\uFc04" + "\u5199\u6d0d\uFAdF\u7E24\u3BAB\ucE11\u8F4E\uB7EB" + "\u3c81\u94F7\uB913\u2cd3\uE76E\uc403\u5644\u7FA9" + "\u2ABB\uc153\udc0B\u9d6c\u3174\uF646\uAc89\u14E1" + "\u163A\u6909\u70B6\ud0Ed\ucc42\u98A4\u285c\uF886";
private static final long[][] C = new long[8][256];
private static final long[] rc = new long[R + 1];
static {
for (int x = 0; x < 256; x++) {
char c = sbox.charAt(x / 2);
long v1 = ((x & 1) == 0) ? c >>> 8 : c & 0xff;
long v2 = v1 << 1;
if (v2 >= 0x100L) {
v2 ^= 0x11dL;
}
long v4 = v2 << 1;
if (v4 >= 0x100L) {
v4 ^= 0x11dL;
}
long v5 = v4 ^ v1;
long v8 = v4 << 1;
if (v8 >= 0x100L) {
v8 ^= 0x11dL;
}
long v9 = v8 ^ v1;
/*
* build the circulant table C[0][x] = S[x].[1, 1, 4, 1, 8, 5, 2,
* 9]:
*/
C[0][x] = (v1 << 56) | (v1 << 48) | (v4 << 40) | (v1 << 32) | (v8 << 24) | (v5 << 16) | (v2 << 8) | (v9);
/*
* build the remaining circulant tables C[t][x] = C[0][x] rotr t
*/
for (int t = 1; t < 8; t++) {
C[t][x] = (C[t - 1][x] >>> 8) | ((C[t - 1][x] << 56));
}
}
/*
* build the round constants:
*/
rc[0] = 0L; /*
* not used (assigment kept only to properly initialize all
* variables)
*/
for (int r = 1; r <= R; r++) {
int i = 8 * (r - 1);
rc[r] = (C[0][i] & 0xff00000000000000L) ^ (C[1][i + 1] & 0x00ff000000000000L) ^ (C[2][i + 2] & 0x0000ff0000000000L) ^ (C[3][i + 3] & 0x000000ff00000000L) ^ (C[4][i + 4] & 0x00000000ff000000L) ^ (C[5][i + 5] & 0x0000000000ff0000L) ^ (C[6][i + 6] & 0x000000000000ff00L) ^ (C[7][i + 7] & 0x00000000000000ffL);
}
}
/**
* Global number of hashed bits (256-bit counter).
*/
protected final byte[] bitLength = new byte[32];
/**
* Buffer of data to hash.
*/
protected final byte[] buffer = new byte[64];
/**
* Current number of bits on the buffer.
*/
protected int bufferBits = 0;
/**
* Current (possibly incomplete) byte slot on the buffer.
*/
protected int bufferPos = 0;
/**
* The hashing state.
*/
protected final long[] hash = new long[8];
protected final long[] K = new long[8];
protected final long[] L = new long[8];
protected final long[] block = new long[8];
protected final long[] state = new long[8];
public Whirlpool() {
}
protected static String display(byte[] array) {
char[] val = new char[2 * array.length];
String hex = "0123456789ABCDEF";
for (int i = 0; i < array.length; i++) {
int b = array[i] & 0xff;
val[2 * i] = hex.charAt(b >>> 4);
val[2 * i + 1] = hex.charAt(b & 15);
}
return String.valueOf(val);
}
/**
* The core Whirlpool transform.
*/
protected void processBuffer() {
/*
* map the buffer to a block:
*/
for (int i = 0, j = 0; i < 8; i++, j += 8) {
block[i] = (((long) buffer[j]) << 56) ^ (((long) buffer[j + 1] & 0xffL) << 48) ^ (((long) buffer[j + 2] & 0xffL) << 40) ^ (((long) buffer[j + 3] & 0xffL) << 32) ^ (((long) buffer[j + 4] & 0xffL) << 24) ^ (((long) buffer[j + 5] & 0xffL) << 16) ^ (((long) buffer[j + 6] & 0xffL) << 8) ^ (((long) buffer[j + 7] & 0xffL));
}
/*
* compute and apply K^0 to the cipher state:
*/
for (int i = 0; i < 8; i++) {
state[i] = block[i] ^ (K[i] = hash[i]);
}
/*
* iterate over all rounds:
*/
for (int r = 1; r <= R; r++) {
/*
* compute K^r from K^{r-1}:
*/
for (int i = 0; i < 8; i++) {
L[i] = 0L;
for (int t = 0, s = 56; t < 8; t++, s -= 8) {
L[i] ^= C[t][(int) (K[(i - t) & 7] >>> s) & 0xff];
}
}
for (int i = 0; i < 8; i++) {
K[i] = L[i];
}
K[0] ^= rc[r];
/*
* apply the r-th round transformation:
*/
for (int i = 0; i < 8; i++) {
L[i] = K[i];
for (int t = 0, s = 56; t < 8; t++, s -= 8) {
L[i] ^= C[t][(int) (state[(i - t) & 7] >>> s) & 0xff];
}
}
for (int i = 0; i < 8; i++) {
state[i] = L[i];
}
}
/*
* apply the Miyaguchi-Preneel compression function:
*/
for (int i = 0; i < 8; i++) {
hash[i] ^= state[i] ^ block[i];
}
}
/**
* Initialize the hashing state.
*/
public void NESSIEinit() {
Arrays.fill(bitLength, (byte) 0);
bufferBits = bufferPos = 0;
buffer[0] = 0;
Arrays.fill(hash, 0L);
}
/**
* Delivers input data to the hashing algorithm.
*
* @param source plaintext data to hash.
* @param sourceBits how many bits of plaintext to process.
* <p>
* This method maintains the invariant: bufferBits &lt; 512
* </p>
*/
public void NESSIEadd(byte[] source, long sourceBits) {
/*
* sourcePos | +-------+-------+------- ||||||||||||||||||||| source
* +-------+-------+-------
* +-------+-------+-------+-------+-------+-------
* |||||||||||||||||||||| buffer
* +-------+-------+-------+-------+-------+------- | bufferPos
*/
int sourcePos = 0; // index of leftmost source byte containing data (1
// to 8 bits).
int sourceGap = (8 - ((int) sourceBits & 7)) & 7; // space on
// source[sourcePos].
int bufferRem = bufferBits & 7; // occupied bits on buffer[bufferPos].
int b;
// tally the length of the added data:
long value = sourceBits;
for (int i = 31, carry = 0; i >= 0; i--) {
carry += (bitLength[i] & 0xff) + ((int) value & 0xff);
bitLength[i] = (byte) carry;
carry >>>= 8;
value >>>= 8;
}
// process data in chunks of 8 bits:
while (sourceBits > 8) { // at least source[sourcePos] and
// source[sourcePos+1] contain data.
// take a byte from the source:
b = ((source[sourcePos] << sourceGap) & 0xff) | ((source[sourcePos + 1] & 0xff) >>> (8 - sourceGap));
if (b < 0 || b >= 256) {
throw new RuntimeException("LOGIC ERROR");
}
// process this byte:
buffer[bufferPos++] |= b >>> bufferRem;
bufferBits += 8 - bufferRem; // bufferBits = 8*bufferPos;
if (bufferBits == 512) {
// process data block:
processBuffer();
// reset buffer:
bufferBits = bufferPos = 0;
}
buffer[bufferPos] = (byte) ((b << (8 - bufferRem)) & 0xff);
bufferBits += bufferRem;
// proceed to remaining data:
sourceBits -= 8;
sourcePos++;
}
// now 0 <= sourceBits <= 8;
// furthermore, all data (if any is left) is in source[sourcePos].
if (sourceBits > 0) {
b = (source[sourcePos] << sourceGap) & 0xff; // bits are
// left-justified on b.
// process the remaining bits:
buffer[bufferPos] |= b >>> bufferRem;
} else {
b = 0;
}
if (bufferRem + sourceBits < 8) {
// all remaining data fits on buffer[bufferPos], and there still
// remains some space.
bufferBits += sourceBits;
} else {
// buffer[bufferPos] is full:
bufferPos++;
bufferBits += 8 - bufferRem; // bufferBits = 8*bufferPos;
sourceBits -= 8 - bufferRem;
// now 0 <= sourceBits < 8; furthermore, all data is in
// source[sourcePos].
if (bufferBits == 512) {
// process data block:
processBuffer();
// reset buffer:
bufferBits = bufferPos = 0;
}
buffer[bufferPos] = (byte) ((b << (8 - bufferRem)) & 0xff);
bufferBits += (int) sourceBits;
}
}
/**
* <p>
* Get the hash value from the hashing state.
* </p>
* <p>
* This method uses the invariant: bufferBits &lt; 512
* </p>
* @param digest byte[]
*/
public void NESSIEfinalize(byte[] digest) {
// append a '1'-bit:
buffer[bufferPos] |= 0x80 >>> (bufferBits & 7);
bufferPos++; // all remaining bits on the current byte are set to zero.
// pad with zero bits to complete 512N + 256 bits:
if (bufferPos > 32) {
while (bufferPos < 64) {
buffer[bufferPos++] = 0;
}
// process data block:
processBuffer();
// reset buffer:
bufferPos = 0;
}
while (bufferPos < 32) {
buffer[bufferPos++] = 0;
}
// append bit length of hashed data:
System.arraycopy(bitLength, 0, buffer, 32, 32);
// process data block:
processBuffer();
// return the completed message digest:
for (int i = 0, j = 0; i < 8; i++, j += 8) {
long h = hash[i];
digest[j] = (byte) (h >>> 56);
digest[j + 1] = (byte) (h >>> 48);
digest[j + 2] = (byte) (h >>> 40);
digest[j + 3] = (byte) (h >>> 32);
digest[j + 4] = (byte) (h >>> 24);
digest[j + 5] = (byte) (h >>> 16);
digest[j + 6] = (byte) (h >>> 8);
digest[j + 7] = (byte) (h);
}
}
/**
* Delivers string input data to the hashing algorithm.
*
* @param source plaintext data to hash (ASCII text string).
* This method maintains the invariant: bufferBits &lt; 512
*/
public void NESSIEadd(String source) {
if (source.length() > 0) {
byte[] data = new byte[source.length()];
for (int i = 0; i < source.length(); i++) {
data[i] = (byte) source.charAt(i);
}
NESSIEadd(data, 8 * data.length);
}
}
@Override
public String computeHash(String password) {
byte[] digest = new byte[DIGESTBYTES];
NESSIEinit();
NESSIEadd(password);
NESSIEfinalize(digest);
return display(digest);
}
}
@@ -0,0 +1,123 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.MessageDigestAlgorithm;
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 java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import static fr.xephi.authme.security.HashUtils.isEqual;
@Recommendation(Usage.ACCEPTABLE)
@HasSalt(value = SaltType.TEXT, length = 9)
// Note ljacqu 20151228: Wordpress 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 Wordpress extends UnsaltedMethod {
private static final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private final SecureRandom randomGen = new SecureRandom();
private String encode64(byte[] src, int count) {
int i, value;
StringBuilder output = new StringBuilder();
i = 0;
if (src.length < count) {
byte[] t = new byte[count];
System.arraycopy(src, 0, t, 0, src.length);
Arrays.fill(t, src.length, count - 1, (byte) 0);
src = t;
}
do {
value = src[i] + (src[i] < 0 ? 256 : 0);
++i;
output.append(itoa64.charAt(value & 63));
if (i < count) {
value |= (src[i] + (src[i] < 0 ? 256 : 0)) << 8;
}
output.append(itoa64.charAt((value >> 6) & 63));
if (i++ >= count) {
break;
}
if (i < count) {
value |= (src[i] + (src[i] < 0 ? 256 : 0)) << 16;
}
output.append(itoa64.charAt((value >> 12) & 63));
if (i++ >= count) {
break;
}
output.append(itoa64.charAt((value >> 18) & 63));
} while (i < count);
return output.toString();
}
private String crypt(String password, String setting) {
String output = "*0";
if (((setting.length() < 2) ? setting : setting.substring(0, 2)).equalsIgnoreCase(output)) {
output = "*1";
}
String id = (setting.length() < 3) ? setting : setting.substring(0, 3);
if (!(id.equals("$P$") || id.equals("$H$"))) {
return output;
}
int countLog2 = itoa64.indexOf(setting.charAt(3));
if (countLog2 < 7 || countLog2 > 30) {
return output;
}
int count = 1 << countLog2;
String salt = setting.substring(4, 4 + 8);
if (salt.length() != 8) {
return output;
}
MessageDigest md = HashUtils.getDigest(MessageDigestAlgorithm.MD5);
byte[] pass = stringToUtf8(password);
byte[] hash = md.digest(stringToUtf8(salt + password));
do {
byte[] t = new byte[hash.length + pass.length];
System.arraycopy(hash, 0, t, 0, hash.length);
System.arraycopy(pass, 0, t, hash.length, pass.length);
hash = md.digest(t);
} while (--count > 0);
output = setting.substring(0, 12);
output += encode64(hash, 16);
return output;
}
private String gensaltPrivate(byte[] input) {
String output = "$P$";
int iterationCountLog2 = 8;
output += itoa64.charAt(Math.min(iterationCountLog2 + 5, 30));
output += encode64(input, 6);
return output;
}
private byte[] stringToUtf8(String string) {
try {
return string.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new UnsupportedOperationException("This system doesn't support UTF-8!", e);
}
}
@Override
public String computeHash(String password) {
byte random[] = new byte[6];
randomGen.nextBytes(random);
return crypt(password, gensaltPrivate(stringToUtf8(new String(random))));
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
String hash = hashedPassword.getHash();
String comparedHash = crypt(password, hash);
return isEqual(hash, comparedHash);
}
}
@@ -0,0 +1,45 @@
package fr.xephi.authme.security.crypts;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import java.util.Locale;
import static fr.xephi.authme.security.HashUtils.isEqual;
@Recommendation(Usage.RECOMMENDED)
public class XAuth extends HexSaltedMethod {
private static String getWhirlpool(String message) {
Whirlpool w = new Whirlpool();
byte[] digest = new byte[Whirlpool.DIGESTBYTES];
w.NESSIEinit();
w.NESSIEadd(message);
w.NESSIEfinalize(digest);
return Whirlpool.display(digest);
}
@Override
public String computeHash(String password, String salt, String name) {
String hash = getWhirlpool(salt + password).toLowerCase(Locale.ROOT);
int saltPos = password.length() >= hash.length() ? hash.length() - 1 : password.length();
return hash.substring(0, saltPos) + salt + hash.substring(saltPos);
}
@Override
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
String hash = hashedPassword.getHash();
int saltPos = password.length() >= hash.length() ? hash.length() - 1 : password.length();
if (saltPos + 12 > hash.length()) {
return false;
}
String salt = hash.substring(saltPos, saltPos + 12);
return isEqual(hash, computeHash(password, salt, name));
}
@Override
public int getSaltLength() {
return 12;
}
}
@@ -0,0 +1,35 @@
package fr.xephi.authme.security.crypts;
import at.favre.lib.crypto.bcrypt.BCrypt;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class XfBCrypt extends BCryptBasedHash {
public static final String SCHEME_CLASS = "XenForo_Authentication_Core12";
private static final Pattern HASH_PATTERN = Pattern.compile("\"hash\";s.*\"(.*)?\"");
XfBCrypt() {
super(new BCryptHasher(BCrypt.Version.VERSION_2A, 10));
}
/**
* Extracts the password hash from the given BLOB.
*
* @param blob the blob to process
* @return the extracted hash
*/
public static String getHashFromBlob(byte[] blob) {
String line = new String(blob);
Matcher m = HASH_PATTERN.matcher(line);
if (m.find()) {
return m.group(1);
}
return "*"; // what?
}
public static String serializeHash(String hash) {
return "a:1:{s:4:\"hash\";s:" + hash.length() + ":\""+hash+"\";}";
}
}
@@ -0,0 +1,15 @@
package fr.xephi.authme.security.crypts.description;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Denotes a hashing algorithm that is restricted to the ASCII charset.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AsciiRestricted {
}
@@ -0,0 +1,30 @@
package fr.xephi.authme.security.crypts.description;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Describes the type of salt the encryption algorithm uses. This is purely for documentation
* purposes and is ignored by the code.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface HasSalt {
/**
* The type of the salt.
*
* @return The salt type
*/
SaltType value();
/**
* For text salts, the length of the salt.
*
* @return The length of the salt the algorithm uses, or 0 if not defined or not applicable.
*/
int length() default 0;
}
@@ -0,0 +1,24 @@
package fr.xephi.authme.security.crypts.description;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to mark a hash algorithm with the usage recommendation.
*
* @see Usage
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Recommendation {
/**
* The recommendation for using the hash algorithm.
*
* @return The recommended usage
*/
Usage value();
}
@@ -0,0 +1,17 @@
package fr.xephi.authme.security.crypts.description;
/**
* The type of salt used by an encryption algorithm.
*/
public enum SaltType {
/** Random, newly generated text. */
TEXT,
/** Salt is based on the username, including variations and repetitions thereof. */
USERNAME,
/** No salt. */
NONE
}
@@ -0,0 +1,29 @@
package fr.xephi.authme.security.crypts.description;
/**
* Usage recommendation that can be provided for a hash algorithm.
* <p>
* Use the following rules of thumb:
* <ul>
* <li>Hashes using MD5 may be {@link #ACCEPTABLE} but never {@link #RECOMMENDED}.</li>
* <li>Hashes using no salt or one based on the username should be {@link #DO_NOT_USE}.</li>
* </ul>
*/
public enum Usage {
/** The hash algorithm appears to be cryptographically secure and is one of the algorithms recommended by AuthMe. */
RECOMMENDED,
/** There are safer algorithms that can be chosen but using the algorithm is generally OK. */
ACCEPTABLE,
/** Hash algorithm is not recommended to be used. Use only if required by another system. */
DO_NOT_USE,
/** Algorithm that is or will be no longer supported actively. */
DEPRECATED,
/** The algorithm does not work properly; do not use. */
DOES_NOT_WORK
}
@@ -0,0 +1,70 @@
package fr.xephi.authme.security.totp;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult;
import fr.xephi.authme.util.expiring.ExpiringMap;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* Handles the generation of new TOTP secrets for players.
*/
public class GenerateTotpService implements HasCleanup {
private static final int NEW_TOTP_KEY_EXPIRATION_MINUTES = 5;
private final ExpiringMap<String, TotpGenerationResult> totpKeys;
@Inject
private TotpAuthenticator totpAuthenticator;
GenerateTotpService() {
this.totpKeys = new ExpiringMap<>(NEW_TOTP_KEY_EXPIRATION_MINUTES, TimeUnit.MINUTES);
}
/**
* Generates a new TOTP key and returns the corresponding QR code.
*
* @param player the player to save the TOTP key for
* @return TOTP generation result
*/
public TotpGenerationResult generateTotpKey(Player player) {
TotpGenerationResult credentials = totpAuthenticator.generateTotpKey(player);
totpKeys.put(player.getName().toLowerCase(Locale.ROOT), credentials);
return credentials;
}
/**
* Returns the generated TOTP secret of a player, if available and not yet expired.
*
* @param player the player to retrieve the TOTP key for
* @return TOTP generation result
*/
public TotpGenerationResult getGeneratedTotpKey(Player player) {
return totpKeys.get(player.getName().toLowerCase(Locale.ROOT));
}
public void removeGenerateTotpKey(Player player) {
totpKeys.remove(player.getName().toLowerCase(Locale.ROOT));
}
/**
* Returns whether the given totp code is correct for the generated TOTP key of the player.
*
* @param player the player to verify the code for
* @param totpCode the totp code to verify with the generated secret
* @return true if the input code is correct, false if the code is invalid or no unexpired totp key is available
*/
public boolean isTotpCodeCorrectForGeneratedTotpKey(Player player, String totpCode) {
TotpGenerationResult totpDetails = totpKeys.get(player.getName().toLowerCase(Locale.ROOT));
return totpDetails != null && totpAuthenticator.checkCode(player.getName(), totpDetails.getTotpKey(), totpCode);
}
@Override
public void performCleanup() {
totpKeys.removeExpiredEntries();
}
}
@@ -0,0 +1,98 @@
package fr.xephi.authme.security.totp;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.primitives.Ints;
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
import com.warrenstrange.googleauth.IGoogleAuthenticator;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Locale;
import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE;
/**
* Provides TOTP functions (wrapping a third-party TOTP implementation).
*/
public class TotpAuthenticator implements HasCleanup {
private static final int CODE_RETENTION_MINUTES = 5;
private final IGoogleAuthenticator authenticator;
private final Settings settings;
private final Table<String, Integer, Long> usedCodes = HashBasedTable.create();
@Inject
TotpAuthenticator(Settings settings) {
this.authenticator = createGoogleAuthenticator();
this.settings = settings;
}
/**
* @return new Google Authenticator instance
*/
protected IGoogleAuthenticator createGoogleAuthenticator() {
return new GoogleAuthenticator();
}
public boolean checkCode(PlayerAuth auth, String totpCode) {
return checkCode(auth.getNickname(), auth.getTotpKey(), totpCode);
}
/**
* Returns whether the given input code matches for the provided TOTP key.
*
* @param playerName the player name
* @param totpKey the key to check with
* @param inputCode the input code to verify
* @return true if code is valid, false otherwise
*/
public boolean checkCode(String playerName, String totpKey, String inputCode) {
String nameLower = playerName.toLowerCase(Locale.ROOT);
Integer totpCode = Ints.tryParse(inputCode);
if (totpCode != null && !usedCodes.contains(nameLower, totpCode)
&& authenticator.authorize(totpKey, totpCode)) {
usedCodes.put(nameLower, totpCode, System.currentTimeMillis());
return true;
}
return false;
}
public TotpGenerationResult generateTotpKey(Player player) {
GoogleAuthenticatorKey credentials = authenticator.createCredentials();
String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL(
settings.getProperty(PluginSettings.SERVER_NAME), player.getName(), credentials);
return new TotpGenerationResult(credentials.getKey(), qrCodeUrl);
}
@Override
public void performCleanup() {
long threshold = System.currentTimeMillis() - CODE_RETENTION_MINUTES * MILLIS_PER_MINUTE;
usedCodes.values().removeIf(value -> value < threshold);
}
public static final class TotpGenerationResult {
private final String totpKey;
private final String authenticatorQrCodeUrl;
public TotpGenerationResult(String totpKey, String authenticatorQrCodeUrl) {
this.totpKey = totpKey;
this.authenticatorQrCodeUrl = authenticatorQrCodeUrl;
}
public String getTotpKey() {
return totpKey;
}
public String getAuthenticatorQrCodeUrl() {
return authenticatorQrCodeUrl;
}
}
}