#472 Store recovery codes in memory instead of in data source

This commit is contained in:
ljacqu
2016-09-16 21:42:16 +02:00
parent bff344ba8f
commit e30d7220bd
13 changed files with 140 additions and 357 deletions
@@ -1,41 +0,0 @@
package fr.xephi.authme.cache.auth;
/**
* Stored data for email recovery.
*/
public class EmailRecoveryData {
private final String email;
private final String recoveryCode;
/**
* Constructor.
*
* @param email the email address
* @param recoveryCode the recovery code, or null if not available
* @param codeExpiration expiration timestamp of the recovery code
*/
public EmailRecoveryData(String email, String recoveryCode, Long codeExpiration) {
this.email = email;
if (codeExpiration == null || System.currentTimeMillis() > codeExpiration) {
this.recoveryCode = null;
} else {
this.recoveryCode = recoveryCode;
}
}
/**
* @return the email address
*/
public String getEmail() {
return email;
}
/**
* @return the recovery code, if available and not expired
*/
public String getRecoveryCode() {
return recoveryCode;
}
}
@@ -1,7 +1,7 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.EmailRecoveryData;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.cache.auth.PlayerCache;
import fr.xephi.authme.command.CommandService;
import fr.xephi.authme.command.PlayerCommand;
@@ -11,15 +11,13 @@ import fr.xephi.authme.output.MessageKey;
import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.security.RandomString;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.properties.EmailSettings;
import fr.xephi.authme.service.RecoveryCodeManager;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import static fr.xephi.authme.settings.properties.SecuritySettings.RECOVERY_CODE_HOURS_VALID;
import static fr.xephi.authme.settings.properties.SecuritySettings.RECOVERY_CODE_LENGTH;
import static fr.xephi.authme.util.Utils.MILLIS_PER_HOUR;
import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH;
/**
* Command for password recovery by email.
@@ -41,6 +39,9 @@ public class RecoverEmailCommand extends PlayerCommand {
@Inject
private SendMailSSL sendMailSsl;
@Inject
private RecoveryCodeManager recoveryCodeManager;
@Override
public void runCommand(Player player, List<String> arguments) {
final String playerMail = arguments.get(0);
@@ -56,48 +57,54 @@ public class RecoverEmailCommand extends PlayerCommand {
return;
}
EmailRecoveryData recoveryData = dataSource.getEmailRecoveryData(playerName);
if (recoveryData == null) {
PlayerAuth auth = dataSource.getAuth(playerName); // TODO: Create method to get email only
if (auth == null) {
commandService.send(player, MessageKey.REGISTER_EMAIL_MESSAGE);
return;
}
final String email = recoveryData.getEmail();
final String email = auth.getEmail();
if (email == null || !email.equalsIgnoreCase(playerMail) || "your@email.com".equalsIgnoreCase(email)) {
commandService.send(player, MessageKey.INVALID_EMAIL);
return;
}
if (arguments.size() == 1) {
// Process /email recover addr@example.com
createAndSendRecoveryCode(playerName, recoveryData);
if (recoveryCodeManager.isRecoveryCodeNeeded()) {
// Process /email recovery addr@example.com
if (arguments.size() == 1) {
createAndSendRecoveryCode(playerName, email);
} else {
// Process /email recovery addr@example.com 12394
processRecoveryCode(player, arguments.get(1), email);
}
} else {
// Process /email recover addr@example.com 12394
processRecoveryCode(player, arguments.get(1), recoveryData);
generateAndSendNewPassword(player, email);
}
}
private void createAndSendRecoveryCode(String name, EmailRecoveryData recoveryData) {
String recoveryCode = RandomString.generateHex(commandService.getProperty(RECOVERY_CODE_LENGTH));
long expiration = System.currentTimeMillis()
+ commandService.getProperty(RECOVERY_CODE_HOURS_VALID) * MILLIS_PER_HOUR;
dataSource.setRecoveryCode(name, recoveryCode, expiration);
sendMailSsl.sendRecoveryCode(name, recoveryData.getEmail(), recoveryCode);
private void createAndSendRecoveryCode(String name, String email) {
String recoveryCode = recoveryCodeManager.generateCode(name);
sendMailSsl.sendRecoveryCode(name, email, recoveryCode);
}
private void processRecoveryCode(Player player, String code, EmailRecoveryData recoveryData) {
if (!code.equals(recoveryData.getRecoveryCode())) {
private void processRecoveryCode(Player player, String code, String email) {
final String name = player.getName();
if (!recoveryCodeManager.isCodeValid(name, code)) {
player.sendMessage("The recovery code is not correct! Use /email recovery [email] to generate a new one");
return;
}
final String name = player.getName();
String thePass = RandomString.generate(commandService.getProperty(EmailSettings.RECOVERY_PASSWORD_LENGTH));
generateAndSendNewPassword(player, email);
recoveryCodeManager.removeCode(name);
}
private void generateAndSendNewPassword(Player player, String email) {
String name = player.getName();
String thePass = RandomString.generate(commandService.getProperty(RECOVERY_PASSWORD_LENGTH));
HashedPassword hashNew = passwordSecurity.computeHash(thePass, name);
dataSource.updatePassword(name, hashNew);
dataSource.removeRecoveryCode(name);
sendMailSsl.sendPasswordMail(name, recoveryData.getEmail(), thePass);
sendMailSsl.sendPasswordMail(name, email, thePass);
commandService.send(player, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE);
}
}
@@ -8,9 +8,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.EmailRecoveryData;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.cache.auth.PlayerCache;
import fr.xephi.authme.security.crypts.HashedPassword;
@@ -236,23 +234,6 @@ public class CacheDataSource implements DataSource {
return source.getAllAuths();
}
@Override
public void setRecoveryCode(String name, String code, long expiration) {
source.setRecoveryCode(name, code, expiration);
cachedAuths.refresh(name);
}
@Override
public EmailRecoveryData getEmailRecoveryData(String name) {
return source.getEmailRecoveryData(name);
}
@Override
public void removeRecoveryCode(String name) {
source.removeRecoveryCode(name);
cachedAuths.refresh(name);
}
@Override
public List<PlayerAuth> getLoggedPlayers() {
return new ArrayList<>(PlayerCache.getInstance().getCache().values());
@@ -22,8 +22,6 @@ public final class Columns {
public final String EMAIL;
public final String ID;
public final String IS_LOGGED;
public final String RECOVERY_CODE;
public final String RECOVERY_EXPIRATION;
public Columns(Settings settings) {
NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
@@ -40,8 +38,6 @@ public final class Columns {
EMAIL = settings.getProperty(DatabaseSettings.MYSQL_COL_EMAIL);
ID = settings.getProperty(DatabaseSettings.MYSQL_COL_ID);
IS_LOGGED = settings.getProperty(DatabaseSettings.MYSQL_COL_ISLOGGED);
RECOVERY_CODE = settings.getProperty(DatabaseSettings.MYSQL_COL_RECOVERY_CODE);
RECOVERY_EXPIRATION = settings.getProperty(DatabaseSettings.MYSQL_COL_RECOVERY_EXPIRATION);
}
}
@@ -1,6 +1,5 @@
package fr.xephi.authme.datasource;
import fr.xephi.authme.cache.auth.EmailRecoveryData;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.security.crypts.HashedPassword;
@@ -195,30 +194,6 @@ public interface DataSource extends Reloadable {
*/
List<PlayerAuth> getAllAuths();
/**
* Set the password recovery code for a user.
*
* @param name The name of the user
* @param code The recovery code
* @param expiration Recovery code expiration (milliseconds timestamp)
*/
void setRecoveryCode(String name, String code, long expiration);
/**
* Get the information necessary for performing a password recovery by email.
*
* @param name The name of the user
* @return The data of the player, or null if player doesn't exist
*/
EmailRecoveryData getEmailRecoveryData(String name);
/**
* Remove the recovery code of a given user.
*
* @param name The name of the user
*/
void removeRecoveryCode(String name);
/**
* Reload the data source.
*/
@@ -1,7 +1,6 @@
package fr.xephi.authme.datasource;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.EmailRecoveryData;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.cache.auth.PlayerCache;
import fr.xephi.authme.security.crypts.HashedPassword;
@@ -469,21 +468,6 @@ public class FlatFile implements DataSource {
throw new UnsupportedOperationException("Flat file no longer supported");
}
@Override
public void setRecoveryCode(String name, String code, long expiration) {
throw new UnsupportedOperationException("Flat file no longer supported");
}
@Override
public EmailRecoveryData getEmailRecoveryData(String name) {
throw new UnsupportedOperationException("Flat file no longer supported");
}
@Override
public void removeRecoveryCode(String name) {
throw new UnsupportedOperationException("Flat file no longer supported");
}
private static PlayerAuth buildAuthFromArray(String[] args) {
// Format allows 2, 3, 4, 7, 8, 9 fields. Anything else is unknown
if (args.length >= 2 && args.length <= 9 && args.length != 5 && args.length != 6) {
@@ -4,7 +4,6 @@ import com.google.common.annotations.VisibleForTesting;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.EmailRecoveryData;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.security.crypts.HashedPassword;
@@ -209,14 +208,6 @@ public class MySQL implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.EMAIL);
}
if (isColumnMissing(md, col.RECOVERY_CODE)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_CODE + " VARCHAR(20);");
}
if (isColumnMissing(md, col.RECOVERY_EXPIRATION)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_EXPIRATION + " BIGINT;");
}
}
ConsoleLogger.info("MySQL setup finished");
}
@@ -865,55 +856,6 @@ public class MySQL implements DataSource {
return auths;
}
@Override
public void setRecoveryCode(String name, String code, long expiration) {
String sql = "UPDATE " + tableName
+ " SET " + col.RECOVERY_CODE + " = ?, "
+ col.RECOVERY_EXPIRATION + " = ?"
+ " WHERE " + col.NAME + " = ?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, code);
pst.setLong(2, expiration);
pst.setString(3, name.toLowerCase());
pst.executeUpdate();
} catch (SQLException e) {
logSqlException(e);
}
}
@Override
public EmailRecoveryData getEmailRecoveryData(String name) {
String sql = "SELECT " + col.EMAIL + ", " + col.RECOVERY_CODE + ", " + col.RECOVERY_EXPIRATION
+ " FROM " + tableName
+ " WHERE " + col.NAME + " = ?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, name.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return new EmailRecoveryData(
rs.getString(col.EMAIL), rs.getString(col.RECOVERY_CODE), rs.getLong(col.RECOVERY_EXPIRATION));
}
}
} catch (SQLException e) {
logSqlException(e);
}
return null;
}
@Override
public void removeRecoveryCode(String name) {
String sql = "UPDATE " + tableName
+ " SET " + col.RECOVERY_CODE + " = NULL, "
+ col.RECOVERY_EXPIRATION + " = NULL"
+ " WHERE " + col.NAME + " = ?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, name.toLowerCase());
pst.executeUpdate();
} catch (SQLException e) {
logSqlException(e);
}
}
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT);
int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP);
@@ -2,7 +2,6 @@ package fr.xephi.authme.datasource;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.EmailRecoveryData;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.Settings;
@@ -128,14 +127,6 @@ public class SQLite implements DataSource {
if (isColumnMissing(md, col.IS_LOGGED)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " INT DEFAULT '0';");
}
if (isColumnMissing(md, col.RECOVERY_CODE)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_CODE + " VARCHAR(20);");
}
if (isColumnMissing(md, col.RECOVERY_EXPIRATION)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.RECOVERY_EXPIRATION + " BIGINT;");
}
}
ConsoleLogger.info("SQLite Setup finished");
}
@@ -595,55 +586,6 @@ public class SQLite implements DataSource {
return auths;
}
@Override
public void setRecoveryCode(String name, String code, long expiration) {
String sql = "UPDATE " + tableName
+ " SET " + col.RECOVERY_CODE + " = ?, "
+ col.RECOVERY_EXPIRATION + " = ?"
+ " WHERE " + col.NAME + " = ?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, code);
pst.setLong(2, expiration);
pst.setString(3, name.toLowerCase());
pst.executeUpdate();
} catch (SQLException e) {
logSqlException(e);
}
}
@Override
public EmailRecoveryData getEmailRecoveryData(String name) {
String sql = "SELECT " + col.EMAIL + ", " + col.RECOVERY_CODE + ", " + col.RECOVERY_EXPIRATION
+ " FROM " + tableName
+ " WHERE " + col.NAME + " = ?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, name.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return new EmailRecoveryData(
rs.getString(col.EMAIL), rs.getString(col.RECOVERY_CODE), rs.getLong(col.RECOVERY_EXPIRATION));
}
}
} catch (SQLException e) {
logSqlException(e);
}
return null;
}
@Override
public void removeRecoveryCode(String name) {
String sql = "UPDATE " + tableName
+ " SET " + col.RECOVERY_CODE + " = NULL, "
+ col.RECOVERY_EXPIRATION + " = NULL"
+ " WHERE " + col.NAME + " = ?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, name.toLowerCase());
pst.executeUpdate();
} catch (SQLException e) {
logSqlException(e);
}
}
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null;
@@ -0,0 +1,73 @@
package fr.xephi.authme.service;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.security.RandomString;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import javax.inject.Inject;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static fr.xephi.authme.settings.properties.SecuritySettings.RECOVERY_CODE_HOURS_VALID;
import static fr.xephi.authme.util.Utils.MILLIS_PER_HOUR;
/**
* Manager for recovery codes.
*/
public class RecoveryCodeManager implements SettingsDependent {
private Map<String, TimedEntry> recoveryCodes = new ConcurrentHashMap<>();
private int recoveryCodeLength;
private long recoveryCodeExpirationMillis;
@Inject
RecoveryCodeManager(Settings settings) {
reload(settings);
}
public boolean isRecoveryCodeNeeded() {
return recoveryCodeExpirationMillis > 0;
}
public String generateCode(String player) {
String code = RandomString.generateHex(recoveryCodeLength);
recoveryCodes.put(player, new TimedEntry(code, System.currentTimeMillis() + recoveryCodeExpirationMillis));
return code;
}
public boolean isCodeValid(String player, String code) {
TimedEntry entry = recoveryCodes.get(player);
if (entry != null) {
return code != null && code.equals(entry.getCode());
}
return false;
}
public void removeCode(String player) {
recoveryCodes.remove(player);
}
@Override
public void reload(Settings settings) {
recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH);
recoveryCodeExpirationMillis = settings.getProperty(RECOVERY_CODE_HOURS_VALID) * MILLIS_PER_HOUR;
}
private static final class TimedEntry {
private final String code;
private final long expiration;
TimedEntry(String code, long expiration) {
this.code = code;
this.expiration = expiration;
}
public String getCode() {
return System.currentTimeMillis() < expiration ? code : null;
}
}
}
@@ -98,14 +98,6 @@ public class DatabaseSettings implements SettingsHolder {
public static final Property<String> MYSQL_COL_GROUP =
newProperty("ExternalBoardOptions.mySQLColumnGroup", "");
@Comment("Column for storing recovery code (when password lost)")
public static final Property<String> MYSQL_COL_RECOVERY_CODE =
newProperty("DataSource.mySQLrecoveryCode", "recoverycode");
@Comment("Column for storing recovery code expiration")
public static final Property<String> MYSQL_COL_RECOVERY_EXPIRATION =
newProperty("DataSource.mySQLrecoveryExpiration", "recoveryexpiration");
private DatabaseSettings() {
}