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..25b3842 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 from a plaintext string. + * + * @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",