Merge pull request #1 from fourbites/master

Implement Argon2 password hashing and verification in user management
This commit is contained in:
Michael Bayne
2025-08-20 07:52:44 -07:00
committed by GitHub
4 changed files with 124 additions and 3 deletions
+6
View File
@@ -63,6 +63,12 @@
<version>2.5</version> <version>2.5</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/de.mkammerer/argon2-jvm -->
<dependency>
<groupId>de.mkammerer</groupId>
<artifactId>argon2-jvm</artifactId>
<version>2.12</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -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);
}
}
+46 -2
View File
@@ -8,11 +8,10 @@ import java.util.List;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.samskivert.jdbc.jora.FieldMask; import com.samskivert.jdbc.jora.FieldMask;
import com.samskivert.servlet.user.Password;
import com.samskivert.servlet.user.User; import com.samskivert.servlet.user.User;
import com.samskivert.util.ArrayIntSet; import com.samskivert.util.ArrayIntSet;
import static com.threerings.user.Log.log; 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. * Set all the spots that this user has recieved.
*/ */
@@ -1589,7 +1589,7 @@ public class OOOUserRepository extends UserRepository
String[] usersSchema = { String[] usersSchema = {
"userId INTEGER(10) PRIMARY KEY NOT NULL AUTO_INCREMENT", "userId INTEGER(10) PRIMARY KEY NOT NULL AUTO_INCREMENT",
"username VARCHAR(255) NOT NULL", "username VARCHAR(255) NOT NULL",
"password VARCHAR(32) NOT NULL", "password VARCHAR(128) NOT NULL",
"email VARCHAR(128) NOT NULL", "email VARCHAR(128) NOT NULL",
"realname VARCHAR(128) NOT NULL", "realname VARCHAR(128) NOT NULL",
"created DATE NOT NULL", "created DATE NOT NULL",
@@ -1605,6 +1605,12 @@ public class OOOUserRepository extends UserRepository
}; };
JDBCUtil.createTableIfMissing(conn, "users", usersSchema, ""); 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 = { String[] historySchema = {
"userId INTEGER(10) PRIMARY KEY NOT NULL", "userId INTEGER(10) PRIMARY KEY NOT NULL",
"username VARCHAR(255) NOT NULL", "username VARCHAR(255) NOT NULL",