Plan/Plan/common/src/main/java/com/djrapitops/plan/utilities/PassEncryptUtil.java

229 lines
7.7 KiB
Java
Raw Normal View History

/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.utilities;
2017-07-22 10:33:38 +02:00
Interface redesign package restructuring (#1146) * command.commands -> command.subcommands * command -> commands * commands -> system.commands * system.locale -> system.settings.locale * system.settings.changes -> system.settings.config.changes * system.settings.paths -> system.settings.config.paths * system.database -> system.storage.database * db -> system.storage.database * system.storage.database.access.queries -> system.storage.database.queries * system.storage.database.access.transactions -> system.storage.database.transactions * system.storage.database.access -> system.storage.database.operation * Moved Query classes to system.storage.database.queries * Moved Executable classes to system.storage.database.transactions * system.storage.database.patches -> system.storage.database.transactions.patches * system.file -> system.storage.file * system.settings.upkeep * system.storage.upkeep * system.server.info -> system.identification * system.importing -> system.gathering.importing * system.listeners -> system.gathering.listeners * system.gathering.timed * Removed duplicate class * data.container -> system.gathering.domain * data.plugin.PluginsConfigSection -> system.settings.config.ExtensionSettings * data.time -> system.gathering.domain * system.afk -> system.gathering.afk * system.cache -> system.gathering.cache * system.status -> system.gathering.listeners * system.export -> system.delivery.export * system.webserver -> system.delivery.webserver * system.json -> system.delivery.rendering.json * utilities.html -> system.delivery.rendering.html * system.delivery.rendering.html.graphs -> system.delivery.rendering.json.graphs * system.delivery.rendering.html.pages -> system.delivery.rendering.pages * system.delivery.upkeep * utilities.file -> system.settings.upkeep * data.store -> system.delivery.domain * system.update -> system.version * api.exceptions -> exceptions * ShutdownHook -> system.gathering * system.HtmlUtilities - > system.delivery.DeliveryUtilities * PeriodicAnalysisTask -> PeriodicServerExportTask * Deprecated APIv4 classes * Removed ServerTaskSystem (Reduces headache) * Moved & Fixed some tests
2019-08-30 11:36:38 +02:00
import com.djrapitops.plan.exceptions.PassEncryptException;
import com.djrapitops.plan.utilities.dev.Untrusted;
import org.apache.commons.lang3.StringUtils;
2018-01-15 14:54:33 +01:00
2017-07-22 10:33:38 +02:00
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
2017-07-22 10:33:38 +02:00
/**
* Password Encryption utility.
2017-07-26 15:46:19 +02:00
* <p>
* <a href="https://github.com/defuse/password-hashing/blob/master/PasswordStorage.java">Based on this code</a>
2017-07-22 10:33:38 +02:00
*
* @author Defuse
*/
public class PassEncryptUtil {
private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
2017-07-22 10:33:38 +02:00
// These constants may be changed without breaking existing hashes.
private static final int SALT_BYTE_SIZE = 24;
private static final int HASH_BYTE_SIZE = 18;
private static final int PBKDF2_ITERATIONS = 64000;
2017-07-22 10:33:38 +02:00
// These constants define the encoding and may not be changed.
private static final int HASH_SECTIONS = 5;
private static final int HASH_ALGORITHM_INDEX = 0;
private static final int ITERATION_INDEX = 1;
private static final int HASH_SIZE_INDEX = 2;
private static final int SALT_INDEX = 3;
private static final int PBKDF2_INDEX = 4;
2017-08-12 15:07:15 +02:00
/**
* Constructor used to hide the public constructor
*/
private PassEncryptUtil() {
throw new IllegalStateException("Utility class");
}
2017-07-22 10:33:38 +02:00
/**
* Create a hash of password + salt.
*
* @param password Password
* @return Hash + salt
* @throws CannotPerformOperationException If the hash creation fails
*/
public static String createHash(@Untrusted String password) {
2017-07-22 10:33:38 +02:00
return createHash(password.toCharArray());
}
private static String createHash(char[] password) {
2017-07-22 10:33:38 +02:00
// Generate a random salt
SecureRandom random = new SecureRandom();
byte[] salt = new byte[SALT_BYTE_SIZE];
random.nextBytes(salt);
// Hash the password
byte[] hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
int hashSize = hash.length;
// format: algorithm:iterations:hashSize:salt:hash
2017-07-23 14:13:14 +02:00
return "sha1:"
2017-07-22 10:33:38 +02:00
+ PBKDF2_ITERATIONS
+ ":" + hashSize
+ ":" + toBase64(salt)
+ ":" + toBase64(hash);
}
/**
* Verify that a password matches a hash.
*
* @param password Password
* @param correctHash hash created with {@link PassEncryptUtil#createHash(String)}
* @return true if match
* @throws CannotPerformOperationException If hashing fails
* @throws InvalidHashException If the hash is missing details.
*/
public static boolean verifyPassword(@Untrusted String password, String correctHash) {
2017-07-22 10:33:38 +02:00
return verifyPassword(password.toCharArray(), correctHash);
}
private static boolean verifyPassword(char[] password, String correctHash) {
2017-07-22 10:33:38 +02:00
// Decode the hash into its parameters
String[] params = StringUtils.split(correctHash, ':');
2017-07-22 10:33:38 +02:00
if (params.length != HASH_SECTIONS) {
throw new InvalidHashException(
"Fields are missing from the password hash."
);
}
// Currently, Java only supports SHA1.
if (!params[HASH_ALGORITHM_INDEX].equals("sha1")) {
throw new CannotPerformOperationException(
);
}
int iterations;
2017-07-22 10:33:38 +02:00
try {
iterations = Integer.parseInt(params[ITERATION_INDEX]);
} catch (NumberFormatException ex) {
throw new InvalidHashException(
"Could not parse the iteration count as an integer.", ex
);
}
if (iterations < 1) {
throw new InvalidHashException(
"Invalid number of iterations. Must be >= 1."
);
}
byte[] salt;
2017-07-22 10:33:38 +02:00
try {
salt = fromBase64(params[SALT_INDEX]);
} catch (IllegalArgumentException ex) {
throw new InvalidHashException(
"Base64 decoding of salt failed.", ex
);
}
byte[] hash;
2017-07-22 10:33:38 +02:00
try {
hash = fromBase64(params[PBKDF2_INDEX]);
} catch (IllegalArgumentException ex) {
throw new InvalidHashException(
"Base64 decoding of pbkdf2 output failed.", ex
);
}
int storedHashSize;
2017-07-22 10:33:38 +02:00
try {
storedHashSize = Integer.parseInt(params[HASH_SIZE_INDEX]);
} catch (NumberFormatException ex) {
throw new InvalidHashException(
"Could not parse the hash size as an integer.", ex
);
}
if (storedHashSize != hash.length) {
throw new InvalidHashException(
"Hash length doesn't match stored hash length."
);
}
2017-07-26 15:46:19 +02:00
// Compute the hash of the provided password, using the same salt,
2017-07-22 10:33:38 +02:00
// iteration count, and hash length
byte[] testHash = pbkdf2(password, salt, iterations, hash.length);
// Compare the hashes in constant time. The password is correct if
// both hashes match.
return slowEquals(hash, testHash);
}
private static boolean slowEquals(byte[] a, byte[] b) {
int diff = a.length ^ b.length;
for (int i = 0; i < a.length && i < b.length; i++) {
diff |= a[i] ^ b[i];
}
return diff == 0;
}
private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes) {
2017-07-22 10:33:38 +02:00
try {
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);
SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
return skf.generateSecret(spec).getEncoded();
} catch (NoSuchAlgorithmException ex) {
throw new CannotPerformOperationException(
"Hash algorithm not supported: " + PBKDF2_ALGORITHM, ex
2017-07-22 10:33:38 +02:00
);
} catch (InvalidKeySpecException ex) {
throw new CannotPerformOperationException(
"Invalid key spec.", ex
);
}
}
private static byte[] fromBase64(String hex) {
return Base64.getDecoder().decode(hex);
2017-07-22 10:33:38 +02:00
}
private static String toBase64(byte[] array) {
return Base64.getEncoder().encodeToString(array);
2017-07-22 10:33:38 +02:00
}
2017-07-26 15:46:19 +02:00
@SuppressWarnings("serial")
2018-01-15 14:54:33 +01:00
public static class InvalidHashException extends PassEncryptException {
2017-07-26 15:46:19 +02:00
InvalidHashException(String message) {
2017-07-26 15:46:19 +02:00
super(message);
}
InvalidHashException(String message, Throwable source) {
2017-07-26 15:46:19 +02:00
super(message, source);
}
}
@SuppressWarnings("serial")
2018-01-15 14:54:33 +01:00
public static class CannotPerformOperationException extends PassEncryptException {
2017-07-26 15:46:19 +02:00
CannotPerformOperationException() {
super("Unsupported hash type.");
2017-07-26 15:46:19 +02:00
}
CannotPerformOperationException(String message, Throwable source) {
2017-07-26 15:46:19 +02:00
super(message, source);
}
}
2017-07-22 10:33:38 +02:00
}