3

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.

Austin Brown
  • 830
  • 12
  • 24

1 Answers1

9

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);
    }
}
Michael Fehr
  • 5,827
  • 2
  • 19
  • 40
  • Interesting, thanks for a lot of detail! If I understand correctly, I need to be using the `Argon2BytesGenerator` class, instead of the `Argon2PasswordEncoder` class? – Austin Brown Mar 12 '21 at 13:23
  • 1
    Yes, Argon2BytesGenerator is the one you need. – Michael Fehr Mar 12 '21 at 14:09
  • Cool, just got the chance to test this out. This looks like the winner to me! – Austin Brown Mar 12 '21 at 20:07
  • I also noticed that the hashes always end in `=`. Do you know why that is? Is that something to be concerned about at all? – Austin Brown Mar 12 '21 at 20:34
  • 2
    No concerning at all - it's because I'm encoding the output in Base64. A 32 byte long output (the encryption key) gets longer due to Base64 encoding and the "=" is a kind of "padding". As all input is 32 byte long the padding is always one "=". – Michael Fehr Mar 12 '21 at 22:02