From 802ec201fa53f25a2a4d1e14059d97b5c3b50cb3 Mon Sep 17 00:00:00 2001 From: fourbites Date: Mon, 18 Aug 2025 23:52:41 +0200 Subject: [PATCH 1/2] Implement Argon2 password hashing and verification in user management --- pom.xml | 6 ++ src/main/java/com/threerings/user/Crypto.java | 65 +++++++++++++++++++ .../java/com/threerings/user/OOOUser.java | 48 +++++++++++++- .../threerings/user/OOOUserRepository.java | 8 ++- 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/threerings/user/Crypto.java diff --git a/pom.xml b/pom.xml index 8b7f1de..50a3f16 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,12 @@ 2.5 provided + + + de.mkammerer + argon2-jvm + 2.12 + diff --git a/src/main/java/com/threerings/user/Crypto.java b/src/main/java/com/threerings/user/Crypto.java new file mode 100644 index 0000000..292db73 --- /dev/null +++ b/src/main/java/com/threerings/user/Crypto.java @@ -0,0 +1,65 @@ +package com.threerings.user; + +import de.mkammerer.argon2.Argon2; +import de.mkammerer.argon2.Argon2Factory; + +public class Crypto { + + protected static final Argon2 argon2 = Argon2Factory.create(); + + private static final int HASH_ITERATIONS = 2; + private static final int MEMORY_COST_KB = 65536; + private static final int PARALLELISM = 1; + + /** + * Hashes the given password using Argon2. + * + * @param password the password to hash + * @return the hashed password + */ + public static String hashPassword(char[] password) { + return argon2.hash(HASH_ITERATIONS, MEMORY_COST_KB, PARALLELISM, password); + } + + /** + * Verifies the given password against the stored hash. + * + * @param password the password to verify + * @param hash the stored hash + * @return true if the password matches the hash, false otherwise + */ + public static boolean verifyPassword(char[] password, String hash) { + return argon2.verify(hash, password); + } + + /** + * Checks if the given hash needs to be rehashed based on the current + * parameters. + * + * @param hash the hash to check + * @return true if the hash needs to be rehashed, false otherwise + */ + public static boolean needsRehash(String hash) { + return argon2.needsRehash(hash, HASH_ITERATIONS, MEMORY_COST_KB, PARALLELISM); + } + + /** + * Checks whether the given hash is hashed using Argon2. + * + * @param hash the hash to check + * @return true if the hash is hashed with Argon2, false otherwise + */ + public static boolean isArgon2Hashed(String hash) { + return hash != null && hash.startsWith("$argon2"); + } + + /** + * Wipes the contents of the given character array. This should be called + * when a plaintext password is no longer needed so it is not kept in memory. + * + * @param array the array to wipe + */ + public static void wipeArray(char[] array) { + argon2.wipeArray(array); + } +} diff --git a/src/main/java/com/threerings/user/OOOUser.java b/src/main/java/com/threerings/user/OOOUser.java index 6a9742f..7142b8e 100644 --- a/src/main/java/com/threerings/user/OOOUser.java +++ b/src/main/java/com/threerings/user/OOOUser.java @@ -8,11 +8,10 @@ import java.util.List; import java.util.StringTokenizer; import com.google.common.collect.ImmutableList; - import com.samskivert.jdbc.jora.FieldMask; +import com.samskivert.servlet.user.Password; import com.samskivert.servlet.user.User; import com.samskivert.util.ArrayIntSet; - import static com.threerings.user.Log.log; /** @@ -341,6 +340,51 @@ public class OOOUser extends User } } + @Override + public void setPassword(String password) { + // we might want to consider disabling this in favor of directly using char[] + // because char[] can be wiped when it's done being used for security, + // but for now, we'll keep it as is to not break existing functionality + setPassword(password.toCharArray()); + } + + /** + * Sets the user's password and wipes the plaintext password from memory. + * + * @param password the password to set + */ + public void setPassword(char[] password) { + String encrypted = Crypto.hashPassword(password); + setPassword(Password.makeFromCrypto(encrypted)); + } + + /** + * Checks the user's password against the provided plaintext password. + * + * @param password the plaintext password to check + * @return true if the password is correct, false otherwise + */ + public boolean checkPassword(char[] password) { + return Crypto.verifyPassword(password, this.password); + } + + /** + * Checks whether the user's password is hashed using Argon2. + */ + public boolean isArgon2Hashed() { + return Crypto.isArgon2Hashed(password); + } + + /** + * Checks if the user's password needs to be rehashed based on the current + * parameters. + * + * @return true if the password needs to be rehashed, false otherwise + */ + public boolean needsRehash() { + return !isArgon2Hashed() || Crypto.needsRehash(this.password); + } + /** * Set all the spots that this user has recieved. */ diff --git a/src/main/java/com/threerings/user/OOOUserRepository.java b/src/main/java/com/threerings/user/OOOUserRepository.java index 8a7fa9f..4e39c84 100644 --- a/src/main/java/com/threerings/user/OOOUserRepository.java +++ b/src/main/java/com/threerings/user/OOOUserRepository.java @@ -1589,7 +1589,7 @@ public class OOOUserRepository extends UserRepository String[] usersSchema = { "userId INTEGER(10) PRIMARY KEY NOT NULL AUTO_INCREMENT", "username VARCHAR(255) NOT NULL", - "password VARCHAR(32) NOT NULL", + "password VARCHAR(128) NOT NULL", "email VARCHAR(128) NOT NULL", "realname VARCHAR(128) NOT NULL", "created DATE NOT NULL", @@ -1605,6 +1605,12 @@ public class OOOUserRepository extends UserRepository }; JDBCUtil.createTableIfMissing(conn, "users", usersSchema, ""); + // give the password a longer length to allow more secure hashing + int passwordLength = JDBCUtil.getColumnSize(conn, "users", "password"); + if (passwordLength < 128) { + JDBCUtil.changeColumn(conn, "users", "password", "password VARCHAR(128) NOT NULL"); + } + String[] historySchema = { "userId INTEGER(10) PRIMARY KEY NOT NULL", "username VARCHAR(255) NOT NULL", From 676a08fa865ff825415c5bb3b327b55d74396a98 Mon Sep 17 00:00:00 2001 From: fourbites Date: Tue, 19 Aug 2025 00:00:27 +0200 Subject: [PATCH 2/2] Update password setting method documentation for clarity --- src/main/java/com/threerings/user/OOOUser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/threerings/user/OOOUser.java b/src/main/java/com/threerings/user/OOOUser.java index 7142b8e..25b3842 100644 --- a/src/main/java/com/threerings/user/OOOUser.java +++ b/src/main/java/com/threerings/user/OOOUser.java @@ -349,7 +349,7 @@ public class OOOUser extends User } /** - * Sets the user's password and wipes the plaintext password from memory. + * Sets the user's password from a plaintext string. * * @param password the password to set */