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",