From 9954c82cb6cbf11ec24a7fe21359eca3ba9bac07 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 5 Mar 2018 19:50:58 +0100 Subject: [PATCH] #1141 Add TOTP key field to database and PlayerAuth - Add new field for storing TOTP key - Implement data source methods for manipulation of its value --- .../fr/xephi/authme/data/auth/PlayerAuth.java | 12 +++++++ .../authme/datasource/CacheDataSource.java | 9 ++++++ .../fr/xephi/authme/datasource/Columns.java | 2 ++ .../xephi/authme/datasource/DataSource.java | 19 ++++++++++++ .../fr/xephi/authme/datasource/FlatFile.java | 5 +++ .../fr/xephi/authme/datasource/MySQL.java | 20 ++++++++++++ .../fr/xephi/authme/datasource/SQLite.java | 21 +++++++++++++ .../settings/properties/DatabaseSettings.java | 4 +++ .../AbstractDataSourceIntegrationTest.java | 31 +++++++++++++++++++ .../authme/datasource/sql-initialize.sql | 5 +-- 10 files changed, 126 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java index 4c8b8ee3..534c0c01 100644 --- a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java +++ b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java @@ -26,6 +26,7 @@ public class PlayerAuth { /** The player's name in the correct casing, e.g. "Xephi". */ private String realName; private HashedPassword password; + private String totpKey; private String email; private String lastIp; private int groupId; @@ -160,6 +161,10 @@ public class PlayerAuth { this.registrationDate = registrationDate; } + public String getTotpKey() { + return totpKey; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof PlayerAuth)) { @@ -195,6 +200,7 @@ public class PlayerAuth { private String name; private String realName; private HashedPassword password; + private String totpKey; private String lastIp; private String email; private int groupId = -1; @@ -219,6 +225,7 @@ public class PlayerAuth { auth.nickname = checkNotNull(name).toLowerCase(); auth.realName = firstNonNull(realName, "Player"); auth.password = firstNonNull(password, new HashedPassword("")); + auth.totpKey = totpKey; auth.email = DB_EMAIL_DEFAULT.equals(email) ? null : email; auth.lastIp = lastIp; // Don't check against default value 127.0.0.1 as it may be a legit value auth.groupId = groupId; @@ -258,6 +265,11 @@ public class PlayerAuth { return password(new HashedPassword(hash, salt)); } + public Builder totpKey(String totpKey) { + this.totpKey = totpKey; + return this; + } + public Builder lastIp(String lastIp) { this.lastIp = lastIp; return this; diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index 39f04a53..0926def8 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -268,6 +268,15 @@ public class CacheDataSource implements DataSource { return source.getRecentlyLoggedInPlayers(); } + @Override + public boolean setTotpKey(String user, String totpKey) { + boolean result = source.setTotpKey(user, totpKey); + if (result) { + cachedAuths.refresh(user); + } + return result; + } + @Override public void invalidateCache(String playerName) { cachedAuths.invalidate(playerName); diff --git a/src/main/java/fr/xephi/authme/datasource/Columns.java b/src/main/java/fr/xephi/authme/datasource/Columns.java index 946c33de..0d372a23 100644 --- a/src/main/java/fr/xephi/authme/datasource/Columns.java +++ b/src/main/java/fr/xephi/authme/datasource/Columns.java @@ -14,6 +14,7 @@ public final class Columns { public final String REAL_NAME; public final String PASSWORD; public final String SALT; + public final String TOTP_KEY; public final String LAST_IP; public final String LAST_LOGIN; public final String GROUP; @@ -35,6 +36,7 @@ public final class Columns { REAL_NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_REALNAME); PASSWORD = settings.getProperty(DatabaseSettings.MYSQL_COL_PASSWORD); SALT = settings.getProperty(DatabaseSettings.MYSQL_COL_SALT); + TOTP_KEY = settings.getProperty(DatabaseSettings.MYSQL_COL_TOTP_KEY); LAST_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_LAST_IP); LAST_LOGIN = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOGIN); GROUP = settings.getProperty(DatabaseSettings.MYSQL_COL_GROUP); diff --git a/src/main/java/fr/xephi/authme/datasource/DataSource.java b/src/main/java/fr/xephi/authme/datasource/DataSource.java index 6f97951d..b6e09fff 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -232,6 +232,25 @@ public interface DataSource extends Reloadable { */ List getRecentlyLoggedInPlayers(); + /** + * Sets the given TOTP key to the player's account. + * + * @param user the name of the player to modify + * @param totpKey the totp key to set + * @return True upon success, false upon failure + */ + boolean setTotpKey(String user, String totpKey); + + /** + * Removes the TOTP key if present of the given player's account. + * + * @param user the name of the player to modify + * @return True upon success, false upon failure + */ + default boolean removeTotpKey(String user) { + return setTotpKey(user, null); + } + /** * Reload the data source. */ diff --git a/src/main/java/fr/xephi/authme/datasource/FlatFile.java b/src/main/java/fr/xephi/authme/datasource/FlatFile.java index d234da55..f70b0376 100644 --- a/src/main/java/fr/xephi/authme/datasource/FlatFile.java +++ b/src/main/java/fr/xephi/authme/datasource/FlatFile.java @@ -398,6 +398,11 @@ public class FlatFile implements DataSource { throw new UnsupportedOperationException("Flat file no longer supported"); } + @Override + public boolean setTotpKey(String user, String totpKey) { + throw new UnsupportedOperationException("Flat file no longer supported"); + } + /** * Creates a PlayerAuth object from the read data. * diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 01a240fe..93014edc 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -248,6 +248,11 @@ public class MySQL implements DataSource { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.IS_LOGGED); } + + if (isColumnMissing(md, col.TOTP_KEY)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);"); + } } ConsoleLogger.info("MySQL setup finished"); } @@ -728,6 +733,20 @@ public class MySQL implements DataSource { return players; } + @Override + public boolean setTotpKey(String user, String totpKey) { + String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, totpKey); + pst.setString(2, user.toLowerCase()); + pst.executeUpdate(); + return true; + } catch (SQLException e) { + logSqlException(e); + } + return false; + } + 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); @@ -735,6 +754,7 @@ public class MySQL implements DataSource { .name(row.getString(col.NAME)) .realName(row.getString(col.REAL_NAME)) .password(row.getString(col.PASSWORD), salt) + .totpKey(row.getString(col.TOTP_KEY)) .lastLogin(getNullableLong(row, col.LAST_LOGIN)) .lastIp(row.getString(col.LAST_IP)) .email(row.getString(col.EMAIL)) diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index 422d57b1..df46f930 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -171,6 +171,11 @@ public class SQLite implements DataSource { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';"); } + + if (isColumnMissing(md, col.TOTP_KEY)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);"); + } } ConsoleLogger.info("SQLite Setup finished"); } @@ -654,6 +659,21 @@ public class SQLite implements DataSource { return players; } + + @Override + public boolean setTotpKey(String user, String totpKey) { + String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, totpKey); + pst.setString(2, user.toLowerCase()); + pst.executeUpdate(); + return true; + } catch (SQLException e) { + logSqlException(e); + } + return false; + } + private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; @@ -662,6 +682,7 @@ public class SQLite implements DataSource { .email(row.getString(col.EMAIL)) .realName(row.getString(col.REAL_NAME)) .password(row.getString(col.PASSWORD), salt) + .totpKey(row.getString(col.TOTP_KEY)) .lastLogin(getNullableLong(row, col.LAST_LOGIN)) .lastIp(row.getString(col.LAST_IP)) .registrationDate(row.getLong(col.REGISTRATION_DATE)) diff --git a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java index 66ddd3cd..8ebccf94 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -79,6 +79,10 @@ public final class DatabaseSettings implements SettingsHolder { public static final Property MYSQL_COL_HASSESSION = newProperty("DataSource.mySQLColumnHasSession", "hasSession"); + @Comment("Column for storing a player's TOTP key (for two-factor authentication)") + public static final Property MYSQL_COL_TOTP_KEY = + newProperty("DataSource.mySQLtotpKey", "totp"); + @Comment("Column for storing the player's last IP") public static final Property MYSQL_COL_LAST_IP = newProperty("DataSource.mySQLColumnIp", "ip"); diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java index 530ab56b..0e809482 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java @@ -103,12 +103,14 @@ public abstract class AbstractDataSourceIntegrationTest { assertThat(bobbyAuth, hasRegistrationInfo("127.0.4.22", 1436778723L)); assertThat(bobbyAuth.getLastLogin(), equalTo(1449136800L)); assertThat(bobbyAuth.getPassword(), equalToHash("$SHA$11aa0706173d7272$dbba966")); + assertThat(bobbyAuth.getTotpKey(), equalTo("JBSWY3DPEHPK3PXP")); assertThat(userAuth, hasAuthBasicData("user", "user", "user@example.org", "34.56.78.90")); assertThat(userAuth, hasAuthLocation(124.1, 76.3, -127.8, "nether", 0.23f, 4.88f)); assertThat(userAuth, hasRegistrationInfo(null, 0)); assertThat(userAuth.getLastLogin(), equalTo(1453242857L)); assertThat(userAuth.getPassword(), equalToHash("b28c32f624a4eb161d6adc9acb5bfc5b", "f750ba32")); + assertThat(userAuth.getTotpKey(), nullValue()); } @Test @@ -494,4 +496,33 @@ public abstract class AbstractDataSourceIntegrationTest { contains("user24", "user20", "user22", "user29", "user28", "user16", "user18", "user12", "user14", "user11")); } + + @Test + public void shouldSetTotpKey() { + // given + DataSource dataSource = getDataSource(); + String newTotpKey = "My new TOTP key"; + + // when + dataSource.setTotpKey("BObBy", newTotpKey); + dataSource.setTotpKey("does-not-exist", "bogus"); + + // then + assertThat(dataSource.getAuth("bobby").getTotpKey(), equalTo(newTotpKey)); + } + + @Test + public void shouldRemoveTotpKey() { + // given + DataSource dataSource = getDataSource(); + + // when + dataSource.removeTotpKey("BoBBy"); + dataSource.removeTotpKey("user"); + dataSource.removeTotpKey("does-not-exist"); + + // then + assertThat(dataSource.getAuth("bobby").getTotpKey(), nullValue()); + assertThat(dataSource.getAuth("user").getTotpKey(), nullValue()); + } } diff --git a/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql b/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql index 306df476..f8153d0d 100644 --- a/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql +++ b/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql @@ -4,6 +4,7 @@ CREATE TABLE authme ( id INTEGER AUTO_INCREMENT, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, + totp VARCHAR(16), ip VARCHAR(40), lastlogin BIGINT, regdate BIGINT NOT NULL, @@ -22,7 +23,7 @@ CREATE TABLE authme ( CONSTRAINT table_const_prim PRIMARY KEY (id) ); -INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate, regip) -VALUES (1,'bobby','$SHA$11aa0706173d7272$dbba966','123.45.67.89',1449136800,1.05,2.1,4.2,'world',-0.44,2.77,'your@email.com',0,'Bobby',NULL,1436778723,'127.0.4.22'); +INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate, regip, totp) +VALUES (1,'bobby','$SHA$11aa0706173d7272$dbba966','123.45.67.89',1449136800,1.05,2.1,4.2,'world',-0.44,2.77,'your@email.com',0,'Bobby',NULL,1436778723,'127.0.4.22','JBSWY3DPEHPK3PXP'); INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate) VALUES (NULL,'user','b28c32f624a4eb161d6adc9acb5bfc5b','34.56.78.90',1453242857,124.1,76.3,-127.8,'nether',0.23,4.88,'user@example.org',0,'user','f750ba32',0);