I'm trying to figure out how to practically use argon2 hashing for passwords in Java. I've got to be missing something, because none of the APIs return discrete fields for the hash or the salt. I've tried both a JVM binding for argon2 and also spring-security + bouncy castle and both give me a String, but it's also serialized with information beyond just the hashed password and salt.
public static void main(String[] args) {
final String rawPass = "badPassword";
// argon2-jvm
Argon2 argon2jvm = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id, 16, 32);
String arg2JvmHash = argon2jvm.hash(10, 65536, 1, rawPass.getBytes(StandardCharsets.UTF_8));
System.out.println("argon2-jvm:");
System.out.println(arg2JvmHash);
System.out.println("\n\n");
// spring security + bouncy castle
Argon2PasswordEncoder arg2SpringSecurity = new Argon2PasswordEncoder(16, 32, 1, 65536, 10);
String springBouncyHash = arg2SpringSecurity.encode(rawPass);
System.out.println("spring security + bouncy castle:");
System.out.println(springBouncyHash);
System.out.println("\n\n");
}
And then here are the results:
argon2-jvm:
$argon2id$v=19$m=65536,t=10,p=1$BeHo0SdgM6vt5risz+yuLg$dOBFlfeoPPGCk/OLCGJ9sRhyPl0zMqMAUZvkltFWxnA
spring security + bouncy castle:
$argon2id$v=19$m=65536,t=10,p=1$i9iHBeHankerOJhfUvXrnQ$8Ldr1QkPglW0DSjYqoaoAy0brxs1vPVhlm4174NdR80
How do I get the discrete values of the hashes and salts? In my research, it sounds like I can parse out this output by myself, but that sounds like a bad idea.
Am I using the wrong libraries? I've been doing a lot of research and these are the two most popular libraries that keep showing up.
I'm using Bouncy Castle to implement Argon2id as it allows to set the parameters and salt instead of parsing the output.
The below full running program uses 4 parameter sets - the parameter were taken from PHP's OpenSSL implementation but you can choose the parameter individually of course.
As the program is taken from a Cross platform project it uses a fixed salt that is UNSECURE - in production you need to use a randomly generated salt.
This is an output:
Generate a 32 byte long encryption key with Argon2id
password: secret password
salt (Base64): AAAAAAAAAAAAAAAAAAAAAA==
encryptionKeyArgon2id (Base64) minimal: e9G7+HHmftUaCEP2O1NwCSJkfyAT0QBzod3Szm1elf0=
encryptionKeyArgon2id (Base64) interactive: FZcsUwo7wf7V24qWTwKeSN9//+Pxy2gCKN35KZX2hXs=
encryptionKeyArgon2id (Base64) moderate: gdizE6kia1W/CgTA3bRKKjtaf8cgZL1BIe6jeDegg0c=
encryptionKeyArgon2id (Base64) sensitive: 19Uym9wI6e/l5f0NocZmNEaouoHvsSyVfrp9iRYl/C8=
code:
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
public class Argon2id {
public static void main(String[] args) {
// uses Bouncy Castle
System.out.println("Generate a 32 byte long encryption key with Argon2id");
String password = "secret password";
System.out.println("password: " + password);
// ### security warning - never use a fixed salt in production, this is for compare reasons only
byte[] salt = generateFixedSalt16Byte();
// please use below generateSalt16Byte()
//byte[] salt = generateSalt16Byte();
System.out.println("salt (Base64): " + base64Encoding(salt));
// ### the minimal parameter set is probably UNSECURE ###
String encryptionKeyArgon2id = base64Encoding(generateArgon2idMinimal(password, salt));
System.out.println("encryptionKeyArgon2id (Base64) minimal: " + encryptionKeyArgon2id);
encryptionKeyArgon2id = base64Encoding(generateArgon2idInteractive(password, salt));
System.out.println("encryptionKeyArgon2id (Base64) interactive: " + encryptionKeyArgon2id);
encryptionKeyArgon2id = base64Encoding(generateArgon2idModerate(password, salt));
System.out.println("encryptionKeyArgon2id (Base64) moderate: " + encryptionKeyArgon2id);
encryptionKeyArgon2id = base64Encoding(generateArgon2idSensitive(password, salt));
System.out.println("encryptionKeyArgon2id (Base64) sensitive: " + encryptionKeyArgon2id);
}
// ### the minimal parameter set is probably UNSECURE ###
public static byte[] generateArgon2idMinimal(String password, byte[] salt) {
int opsLimit = 2;
int memLimit = 8192;
int outputLength = 32;
int parallelism = 1;
Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withVersion(Argon2Parameters.ARGON2_VERSION_13) // 19
.withIterations(opsLimit)
.withMemoryAsKB(memLimit)
.withParallelism(parallelism)
.withSalt(salt);
Argon2BytesGenerator gen = new Argon2BytesGenerator();
gen.init(builder.build());
byte[] result = new byte[outputLength];
gen.generateBytes(password.getBytes(StandardCharsets.UTF_8), result, 0, result.length);
return result;
}
public static byte[] generateArgon2idInteractive(String password, byte[] salt) {
int opsLimit = 2;
int memLimit = 66536;
int outputLength = 32;
int parallelism = 1;
Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withVersion(Argon2Parameters.ARGON2_VERSION_13) // 19
.withIterations(opsLimit)
.withMemoryAsKB(memLimit)
.withParallelism(parallelism)
.withSalt(salt);
Argon2BytesGenerator gen = new Argon2BytesGenerator();
gen.init(builder.build());
byte[] result = new byte[outputLength];
gen.generateBytes(password.getBytes(StandardCharsets.UTF_8), result, 0, result.length);
return result;
}
public static byte[] generateArgon2idModerate(String password, byte[] salt) {
int opsLimit = 3;
int memLimit = 262144;
int outputLength = 32;
int parallelism = 1;
Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withVersion(Argon2Parameters.ARGON2_VERSION_13) // 19
.withIterations(opsLimit)
.withMemoryAsKB(memLimit)
.withParallelism(parallelism)
.withSalt(salt);
Argon2BytesGenerator gen = new Argon2BytesGenerator();
gen.init(builder.build());
byte[] result = new byte[outputLength];
gen.generateBytes(password.getBytes(StandardCharsets.UTF_8), result, 0, result.length);
return result;
}
public static byte[] generateArgon2idSensitive(String password, byte[] salt) {
int opsLimit = 4;
int memLimit = 1048576;
int outputLength = 32;
int parallelism = 1;
Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withVersion(Argon2Parameters.ARGON2_VERSION_13) // 19
.withIterations(opsLimit)
.withMemoryAsKB(memLimit)
.withParallelism(parallelism)
.withSalt(salt);
Argon2BytesGenerator gen = new Argon2BytesGenerator();
gen.init(builder.build());
byte[] result = new byte[outputLength];
gen.generateBytes(password.getBytes(StandardCharsets.UTF_8), result, 0, result.length);
return result;
}
private static byte[] generateSalt16Byte() {
SecureRandom secureRandom = new SecureRandom();
byte[] salt = new byte[16];
secureRandom.nextBytes(salt);
return salt;
}
private static byte[] generateFixedSalt16Byte() {
// ### security warning - never use this in production ###
byte[] salt = new byte[16]; // 16 x0's
return salt;
}
private static String base64Encoding(byte[] input) {
return Base64.getEncoder().encodeToString(input);
}
}