#358 Remove old methods on PasswordSecurity, unify hash + salt
- For encryption methods with a separate salt, the hash is useless without the salt, so hash and salt should always be persisted and retrieved together
This commit is contained in:
parent
ce6951bcfe
commit
9c4a578bec
@ -121,13 +121,8 @@ public class API {
|
|||||||
* @return true if the password is correct, false otherwise
|
* @return true if the password is correct, false otherwise
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public static boolean checkPassword(String playerName,
|
public static boolean checkPassword(String playerName, String passwordToCheck) {
|
||||||
String passwordToCheck) {
|
return isRegistered(playerName) && passwordSecurity.comparePassword(passwordToCheck, playerName);
|
||||||
if (!isRegistered(playerName))
|
|
||||||
return false;
|
|
||||||
String player = playerName.toLowerCase();
|
|
||||||
PlayerAuth auth = instance.getDataSource().getAuth(player);
|
|
||||||
return passwordSecurity.comparePassword(passwordToCheck, auth.getHash(), playerName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -116,9 +116,11 @@ public class NewAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param playerName
|
* Return whether the player is registered.
|
||||||
*
|
*
|
||||||
* @return true if player is registered
|
* @param playerName The player name to check
|
||||||
|
*
|
||||||
|
* @return true if player is registered, false otherwise
|
||||||
*/
|
*/
|
||||||
public boolean isRegistered(String playerName) {
|
public boolean isRegistered(String playerName) {
|
||||||
String player = playerName.toLowerCase();
|
String player = playerName.toLowerCase();
|
||||||
@ -134,12 +136,7 @@ public class NewAPI {
|
|||||||
* @return true if the password is correct, false otherwise
|
* @return true if the password is correct, false otherwise
|
||||||
*/
|
*/
|
||||||
public boolean checkPassword(String playerName, String passwordToCheck) {
|
public boolean checkPassword(String playerName, String passwordToCheck) {
|
||||||
if (!isRegistered(playerName)) {
|
return isRegistered(playerName) && plugin.getPasswordSecurity().comparePassword(passwordToCheck, playerName);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String player = playerName.toLowerCase();
|
|
||||||
PlayerAuth auth = plugin.getDataSource().getAuth(player);
|
|
||||||
return plugin.getPasswordSecurity().comparePassword(passwordToCheck, auth.getHash(), playerName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -136,9 +136,9 @@ public class AsynchronousLogin {
|
|||||||
if (pAuth == null || needsCaptcha())
|
if (pAuth == null || needsCaptcha())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
String hash = pAuth.getHash();
|
|
||||||
String email = pAuth.getEmail();
|
String email = pAuth.getEmail();
|
||||||
boolean passwordVerified = forceLogin || plugin.getPasswordSecurity().comparePassword(password, hash, realName);
|
boolean passwordVerified = forceLogin || plugin.getPasswordSecurity()
|
||||||
|
.comparePassword(password, pAuth.getHash(), pAuth.getSalt(), realName);
|
||||||
|
|
||||||
if (passwordVerified && player.isOnline()) {
|
if (passwordVerified && player.isOnline()) {
|
||||||
PlayerAuth auth = PlayerAuth.builder()
|
PlayerAuth auth = PlayerAuth.builder()
|
||||||
@ -147,7 +147,7 @@ public class AsynchronousLogin {
|
|||||||
.ip(getIP())
|
.ip(getIP())
|
||||||
.lastLogin(new Date().getTime())
|
.lastLogin(new Date().getTime())
|
||||||
.email(email)
|
.email(email)
|
||||||
.hash(hash)
|
.hash(pAuth.getHash())
|
||||||
.salt(pAuth.getSalt())
|
.salt(pAuth.getSalt())
|
||||||
.build();
|
.build();
|
||||||
database.updateSession(auth);
|
database.updateSession(auth);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package fr.xephi.authme.process.unregister;
|
|||||||
|
|
||||||
import fr.xephi.authme.AuthMe;
|
import fr.xephi.authme.AuthMe;
|
||||||
import fr.xephi.authme.ConsoleLogger;
|
import fr.xephi.authme.ConsoleLogger;
|
||||||
|
import fr.xephi.authme.cache.auth.PlayerAuth;
|
||||||
import fr.xephi.authme.cache.auth.PlayerCache;
|
import fr.xephi.authme.cache.auth.PlayerCache;
|
||||||
import fr.xephi.authme.cache.backup.JsonCache;
|
import fr.xephi.authme.cache.backup.JsonCache;
|
||||||
import fr.xephi.authme.cache.limbo.LimboCache;
|
import fr.xephi.authme.cache.limbo.LimboCache;
|
||||||
@ -52,7 +53,9 @@ public class AsynchronousUnregister {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void process() {
|
public void process() {
|
||||||
if (force || plugin.getPasswordSecurity().comparePassword(password, PlayerCache.getInstance().getAuth(name).getHash(), player.getName())) {
|
PlayerAuth cachedAuth = PlayerCache.getInstance().getAuth(name);
|
||||||
|
if (force || plugin.getPasswordSecurity().comparePassword(
|
||||||
|
password, cachedAuth.getHash(), cachedAuth.getSalt(), player.getName())) {
|
||||||
if (!plugin.getDataSource().removeAuth(name)) {
|
if (!plugin.getDataSource().removeAuth(name)) {
|
||||||
m.send(player, MessageKey.ERROR);
|
m.send(player, MessageKey.ERROR);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,23 +1,19 @@
|
|||||||
package fr.xephi.authme.security;
|
package fr.xephi.authme.security;
|
||||||
|
|
||||||
import fr.xephi.authme.AuthMe;
|
|
||||||
import fr.xephi.authme.cache.auth.PlayerAuth;
|
import fr.xephi.authme.cache.auth.PlayerAuth;
|
||||||
import fr.xephi.authme.datasource.DataSource;
|
import fr.xephi.authme.datasource.DataSource;
|
||||||
import fr.xephi.authme.events.PasswordEncryptionEvent;
|
import fr.xephi.authme.events.PasswordEncryptionEvent;
|
||||||
import fr.xephi.authme.security.crypts.EncryptionMethod;
|
import fr.xephi.authme.security.crypts.EncryptionMethod;
|
||||||
import fr.xephi.authme.security.crypts.HashResult;
|
import fr.xephi.authme.security.crypts.HashResult;
|
||||||
import fr.xephi.authme.settings.Settings;
|
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Manager class for password-related operations.
|
||||||
*/
|
*/
|
||||||
public class PasswordSecurity {
|
public class PasswordSecurity {
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
public static final HashMap<String, String> userSalt = new HashMap<>();
|
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
private final HashAlgorithm algorithm;
|
private final HashAlgorithm algorithm;
|
||||||
private final boolean supportOldAlgorithm;
|
private final boolean supportOldAlgorithm;
|
||||||
@ -28,49 +24,6 @@ public class PasswordSecurity {
|
|||||||
this.supportOldAlgorithm = supportOldAlgorithm;
|
this.supportOldAlgorithm = supportOldAlgorithm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
public static boolean comparePasswordWithHash(String password, String hash,
|
|
||||||
String playerName) throws NoSuchAlgorithmException {
|
|
||||||
HashAlgorithm algorithm = Settings.getPasswordHash;
|
|
||||||
EncryptionMethod method;
|
|
||||||
try {
|
|
||||||
if (algorithm != HashAlgorithm.CUSTOM) {
|
|
||||||
method = algorithm.getClazz().newInstance();
|
|
||||||
} else {
|
|
||||||
method = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
PasswordEncryptionEvent event = new PasswordEncryptionEvent(method, playerName);
|
|
||||||
Bukkit.getPluginManager().callEvent(event);
|
|
||||||
method = event.getMethod();
|
|
||||||
|
|
||||||
if (method == null)
|
|
||||||
throw new NoSuchAlgorithmException("Unknown hash algorithm");
|
|
||||||
|
|
||||||
String salt = null;
|
|
||||||
if (method.hasSeparateSalt()) {
|
|
||||||
PlayerAuth auth = AuthMe.getInstance().getDataSource().getAuth(playerName);
|
|
||||||
if (auth == null) {
|
|
||||||
// User is not in data source, so the result will invariably be wrong because an encryption
|
|
||||||
// method with hasSeparateSalt() == true NEEDS the salt to evaluate the password
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
salt = auth.getSalt();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (method.comparePassword(hash, password, salt, playerName))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (Settings.supportOldPassword) {
|
|
||||||
if (compareWithAllEncryptionMethod(password, hash, playerName))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (InstantiationException | IllegalAccessException e) {
|
|
||||||
throw new NoSuchAlgorithmException("Problem with this hash algorithm");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HashResult computeHash(String password, String playerName) {
|
public HashResult computeHash(String password, String playerName) {
|
||||||
return computeHash(algorithm, password, playerName);
|
return computeHash(algorithm, password, playerName);
|
||||||
}
|
}
|
||||||
@ -80,71 +33,92 @@ public class PasswordSecurity {
|
|||||||
return method.computeHash(password, playerName);
|
return method.computeHash(password, playerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean comparePassword(String hash, String password, String playerName) {
|
public boolean comparePassword(String password, String playerName) {
|
||||||
return comparePassword(algorithm, hash, password, playerName);
|
PlayerAuth auth = dataSource.getAuth(playerName);
|
||||||
|
if (auth != null) {
|
||||||
|
return comparePassword(auth.getHash(), auth.getSalt(), password, playerName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean comparePassword(HashAlgorithm algorithm, String hash, String password, String playerName) {
|
public boolean comparePassword(String hash, String salt, String password, String playerName) {
|
||||||
EncryptionMethod method = initializeEncryptionMethod(algorithm, playerName);
|
EncryptionMethod method = initializeEncryptionMethod(algorithm, playerName);
|
||||||
String salt = null;
|
// User is not in data source, so the result will invariably be wrong because an encryption
|
||||||
if (method.hasSeparateSalt()) {
|
// method with hasSeparateSalt() == true NEEDS the salt to evaluate the password
|
||||||
PlayerAuth auth = dataSource.getAuth(playerName);
|
if (method.hasSeparateSalt() && salt == null) {
|
||||||
if (auth == null) {
|
return false;
|
||||||
// User is not in data source, so the result will invariably be wrong because an encryption
|
|
||||||
// method with hasSeparateSalt() == true NEEDS the salt to evaluate the password
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
salt = auth.getSalt();
|
|
||||||
}
|
}
|
||||||
return method.comparePassword(hash, password, salt, playerName);
|
return method.comparePassword(hash, password, salt, playerName)
|
||||||
// TODO #358: Add logic for Settings.supportOldPassword
|
|| supportOldAlgorithm && compareWithAllEncryptionMethods(password, hash, salt, playerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private EncryptionMethod initializeEncryptionMethod(HashAlgorithm algorithm, String playerName) {
|
/**
|
||||||
EncryptionMethod method;
|
* Compare the given hash with all available encryption methods to support the migration to a new encryption method.
|
||||||
try {
|
*
|
||||||
method = HashAlgorithm.CUSTOM.equals(algorithm)
|
* @param password The clear-text password to check
|
||||||
? null
|
* @param hash The hash to text the password against
|
||||||
: algorithm.getClazz().newInstance();
|
* @param salt The salt (or null if none available)
|
||||||
} catch (InstantiationException | IllegalAccessException e) {
|
* @param playerName The name of the player
|
||||||
throw new IllegalStateException("Constructor for '" + algorithm.getClazz()
|
* @return True if the
|
||||||
+ "' could not be invoked. (Is there no default constructor?)", e);
|
*/
|
||||||
}
|
private boolean compareWithAllEncryptionMethods(String password, String hash, String salt, String playerName) {
|
||||||
|
for (HashAlgorithm algorithm : HashAlgorithm.values()) {
|
||||||
PasswordEncryptionEvent event = new PasswordEncryptionEvent(method, playerName);
|
if (!HashAlgorithm.CUSTOM.equals(algorithm)) {
|
||||||
Bukkit.getPluginManager().callEvent(event);
|
EncryptionMethod method = initializeEncryptionMethodWithoutEvent(algorithm);
|
||||||
return event.getMethod();
|
if (method != null && method.comparePassword(hash, password, salt, playerName)) {
|
||||||
}
|
hashPasswordForNewAlgorithm(password, playerName);
|
||||||
|
return true;
|
||||||
@Deprecated
|
|
||||||
private static boolean compareWithAllEncryptionMethod(String password,
|
|
||||||
String hash, String playerName) {
|
|
||||||
String salt;
|
|
||||||
PlayerAuth auth = AuthMe.getInstance().getDataSource().getAuth(playerName);
|
|
||||||
if (auth == null) {
|
|
||||||
salt = null;
|
|
||||||
} else {
|
|
||||||
salt = auth.getSalt();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (HashAlgorithm algo : HashAlgorithm.values()) {
|
|
||||||
if (algo != HashAlgorithm.CUSTOM) {
|
|
||||||
try {
|
|
||||||
EncryptionMethod method = algo.getClazz().newInstance();
|
|
||||||
if (method.comparePassword(hash, password, salt, playerName)) {
|
|
||||||
PlayerAuth nAuth = AuthMe.getInstance().getDataSource().getAuth(playerName);
|
|
||||||
if (nAuth != null) {
|
|
||||||
// nAuth.setHash(getHash(Settings.getPasswordHash, password, playerName));
|
|
||||||
nAuth.setSalt(userSalt.containsKey(playerName) ? userSalt.get(playerName) : "");
|
|
||||||
AuthMe.getInstance().getDataSource().updatePassword(nAuth);
|
|
||||||
AuthMe.getInstance().getDataSource().updateSalt(nAuth);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the encryption method from the given {@link HashAlgorithm} value and emits a
|
||||||
|
* {@link PasswordEncryptionEvent}. The encryption method from the event is returned,
|
||||||
|
* which may have been changed by an external listener.
|
||||||
|
*
|
||||||
|
* @param algorithm The algorithm to retrieve the encryption method for
|
||||||
|
* @param playerName The name of the player a password will be hashed for
|
||||||
|
* @return The encryption method
|
||||||
|
*/
|
||||||
|
private static EncryptionMethod initializeEncryptionMethod(HashAlgorithm algorithm, String playerName) {
|
||||||
|
EncryptionMethod method = initializeEncryptionMethodWithoutEvent(algorithm);
|
||||||
|
PasswordEncryptionEvent event = new PasswordEncryptionEvent(method, playerName);
|
||||||
|
Bukkit.getPluginManager().callEvent(event);
|
||||||
|
return event.getMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the encryption method corresponding to the given hash algorithm.
|
||||||
|
*
|
||||||
|
* @param algorithm The algorithm to retrieve the encryption method for
|
||||||
|
* @return The associated encryption method
|
||||||
|
*/
|
||||||
|
private static EncryptionMethod initializeEncryptionMethodWithoutEvent(HashAlgorithm algorithm) {
|
||||||
|
try {
|
||||||
|
return HashAlgorithm.CUSTOM.equals(algorithm)
|
||||||
|
? null
|
||||||
|
: algorithm.getClazz().newInstance();
|
||||||
|
} catch (InstantiationException | IllegalAccessException e) {
|
||||||
|
throw new UnsupportedOperationException("Constructor for '" + algorithm.getClazz().getSimpleName()
|
||||||
|
+ "' could not be invoked. (Is there no default constructor?)", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hashPasswordForNewAlgorithm(String password, String playerName) {
|
||||||
|
PlayerAuth auth = dataSource.getAuth(playerName);
|
||||||
|
if (auth != null) {
|
||||||
|
HashResult hashResult = initializeEncryptionMethod(algorithm, playerName)
|
||||||
|
.computeHash(password, playerName);
|
||||||
|
|
||||||
|
// TODO #358: updatePassword() should just take the HashResult..., or at least hash & salt. Idem for setHash
|
||||||
|
auth.setSalt(hashResult.getSalt());
|
||||||
|
auth.setHash(hashResult.getHash());
|
||||||
|
dataSource.updatePassword(auth);
|
||||||
|
dataSource.updateSalt(auth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,8 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@Recommendation(Usage.ACCEPTABLE)
|
// TODO #391: Wordpress algorithm fails sometimes. Fix it and change the Recommendation to "ACCEPTABLE" if appropriate
|
||||||
|
@Recommendation(Usage.DO_NOT_USE)
|
||||||
@HasSalt(value = SaltType.TEXT, length = 9)
|
@HasSalt(value = SaltType.TEXT, length = 9)
|
||||||
// Note ljacqu 20151228: Wordpress is actually a salted algorithm but salt generation is handled internally
|
// 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
|
// and isn't exposed to the outside, so we treat it as an unsalted implementation
|
||||||
|
|||||||
@ -34,7 +34,7 @@ public class ChangePasswordTask implements Runnable {
|
|||||||
|
|
||||||
final String name = player.getName().toLowerCase();
|
final String name = player.getName().toLowerCase();
|
||||||
PlayerAuth auth = PlayerCache.getInstance().getAuth(name);
|
PlayerAuth auth = PlayerCache.getInstance().getAuth(name);
|
||||||
if (passwordSecurity.comparePassword(oldPassword, auth.getHash(), player.getName())) {
|
if (passwordSecurity.comparePassword(oldPassword, auth.getHash(), auth.getSalt(), player.getName())) {
|
||||||
HashResult hashResult = passwordSecurity.computeHash(newPassword, name);
|
HashResult hashResult = passwordSecurity.computeHash(newPassword, name);
|
||||||
auth.setHash(hashResult.getHash());
|
auth.setHash(hashResult.getHash());
|
||||||
auth.setSalt(hashResult.getSalt());
|
auth.setSalt(hashResult.getSalt());
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user