Repository: firebase/firebase-admin-java
Pull request #637 (2022-02-07 19:46:43)

Add Argon2 Hashing Algorithm support
Hey there! So you want to contribute to a Firebase SDK? 
Before you file this pull request, please read these guidelines:

### Discussion

* Issue tracked in #636

### Testing

  * Added unit tests.
  * Added checks for invalid inputs

### API Changes

  * Adds new API for Argon2 Hashing support

prameshj: Looks good to me.

Patch:
diff --git a/pom.xml b/pom.xml
index f13af91..ea82686 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,7 +59,7 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <skipUTs>${skipTests}</skipUTs>
-        <netty.version>4.1.72.Final</netty.version>
+        <netty.version>4.1.75.Final</netty.version>
     </properties>
 
     <scm>
@@ -189,7 +189,7 @@
                     <plugin>
                         <groupId>org.jacoco</groupId>
                         <artifactId>jacoco-maven-plugin</artifactId>
-                        <version>0.8.7</version>
+                        <version>0.8.8</version>
                         <executions>
                             <execution>
                                 <id>pre-unit-test</id>
@@ -367,7 +367,7 @@
         <plugins>
             <plugin>
                 <artifactId>maven-project-info-reports-plugin</artifactId>
-                <version>3.1.2</version>
+                <version>3.2.2</version>
                 <reportSets>
                     <reportSet>
                         <configuration>
@@ -392,7 +392,7 @@
             <dependency>
                 <groupId>com.google.api-client</groupId>
                 <artifactId>google-api-client-bom</artifactId>
-                <version>1.33.1</version>
+                <version>1.33.4</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
@@ -438,7 +438,7 @@
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
-            <version>1.7.33</version>
+            <version>1.7.36</version>
         </dependency>
         <dependency>
             <groupId>io.netty</groupId>
diff --git a/src/main/java/com/google/firebase/auth/hash/Argon2.java b/src/main/java/com/google/firebase/auth/hash/Argon2.java
new file mode 100644
index 0000000..8edcf11
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/Argon2.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2022 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth.hash;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import com.google.firebase.auth.UserImportHash;
+import java.util.Map;
+
+/**
+ * Represents the Argon2 password hashing algorithm. Can be used as an instance of {@link
+ * com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public final class Argon2 extends UserImportHash {
+
+  private static final int MIN_HASH_LENGTH_BYTES = 4;
+  private static final int MAX_HASH_LENGTH_BYTES = 1024;
+  private static final int MIN_PARALLELISM = 1;
+  private static final int MAX_PARALLELISM = 16;
+  private static final int MIN_ITERATIONS = 1;
+  private static final int MAX_ITERATIONS = 16;
+  private static final int MIN_MEMORY_COST_KIB = 1;
+  private static final int MAX_MEMORY_COST_KIB = 32768;
+
+  private final int hashLengthBytes;
+  private final Argon2HashType hashType;
+  private final int parallelism;
+  private final int iterations;
+  private final int memoryCostKib;
+  private final Argon2Version version;
+  private final String associatedData;
+
+  private Argon2(Builder builder) {
+    super("ARGON2");
+    checkArgument(intShouldBeBetweenLimitsInclusive(builder.hashLengthBytes, MIN_HASH_LENGTH_BYTES,
+            MAX_HASH_LENGTH_BYTES),
+        "hashLengthBytes is required for Argon2 and must be between %s and %s",
+        MIN_HASH_LENGTH_BYTES, MAX_HASH_LENGTH_BYTES);
+    checkArgument(builder.hashType != null,
+        "A hashType is required for Argon2");
+    checkArgument(
+        intShouldBeBetweenLimitsInclusive(builder.parallelism, MIN_PARALLELISM, MAX_PARALLELISM),
+        "parallelism is required for Argon2 and must be between %s and %s", MIN_PARALLELISM,
+        MAX_PARALLELISM);
+    checkArgument(
+        intShouldBeBetweenLimitsInclusive(builder.iterations, MIN_ITERATIONS, MAX_ITERATIONS),
+        "iterations is required for Argon2 and must be between %s and %s", MIN_ITERATIONS,
+        MAX_ITERATIONS);
+    checkArgument(intShouldBeBetweenLimitsInclusive(builder.memoryCostKib, MIN_MEMORY_COST_KIB,
+            MAX_MEMORY_COST_KIB),
+        "memoryCostKib is required for Argon2 and must be less than or equal to %s",
+        MAX_MEMORY_COST_KIB);
+    this.hashLengthBytes = builder.hashLengthBytes;
+    this.hashType = builder.hashType;
+    this.parallelism = builder.parallelism;
+    this.iterations = builder.iterations;
+    this.memoryCostKib = builder.memoryCostKib;
+    if (builder.version != null) {
+      this.version = builder.version;
+    } else {
+      this.version = null;
+    }
+    if (builder.associatedData != null) {
+      this.associatedData = BaseEncoding.base64Url().encode(builder.associatedData);
+    } else {
+      this.associatedData = null;
+    }
+  }
+
+  private static boolean intShouldBeBetweenLimitsInclusive(int property, int fromInclusive,
+      int toInclusive) {
+    return property >= fromInclusive && property <= toInclusive;
+  }
+
+  @Override
+  protected Map<String, Object> getOptions() {
+    ImmutableMap.Builder<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
+        .put("hashLengthBytes", hashLengthBytes)
+        .put("hashType", hashType.toString())
+        .put("parallelism", parallelism)
+        .put("iterations", iterations)
+        .put("memoryCostKib", memoryCostKib);
+    if (this.associatedData != null) {
+      argon2Parameters.put("associatedData", associatedData);
+    }
+    if (this.version != null) {
+      argon2Parameters.put("version", version.toString());
+    }
+    return ImmutableMap.<String, Object>of("argon2Parameters", argon2Parameters.build());
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private int hashLengthBytes;
+    private Argon2HashType hashType;
+    private int parallelism;
+    private int iterations;
+    private int memoryCostKib;
+    private Argon2Version version;
+    private byte[] associatedData;
+
+    private Builder() {}
+
+    /**
+     * Sets the hash length in bytes. Required field.
+     *
+     * @param hashLengthBytes an integer between 4 and 1024 (inclusive).
+     * @return This builder.
+     */
+    public Builder setHashLengthBytes(int hashLengthBytes) {
+      this.hashLengthBytes = hashLengthBytes;
+      return this;
+    }
+
+    /**
+     * Sets the Argon2 hash type. Required field.
+     *
+     * @param hashType a value from the {@link Argon2HashType} enum.
+     * @return This builder.
+     */
+    public Builder setHashType(Argon2HashType hashType) {
+      this.hashType = hashType;
+      return this;
+    }
+
+    /**
+     * Sets the degree of parallelism, also called threads or lanes. Required field.
+     *
+     * @param parallelism an integer between 1 and 16 (inclusive).
+     * @return This builder.
+     */
+    public Builder setParallelism(int parallelism) {
+      this.parallelism = parallelism;
+      return this;
+    }
+
+    /**
+     * Sets the number of iterations to perform. Required field.
+     *
+     * @param iterations an integer between 1 and 16 (inclusive).
+     * @return This builder.
+     */
+    public Builder setIterations(int iterations) {
+      this.iterations = iterations;
+      return this;
+    }
+
+    /**
+     * Sets the memory cost in kibibytes. Required field.
+     *
+     * @param memoryCostKib an integer between 1 and 32768 (inclusive).
+     * @return This builder.
+     */
+    public Builder setMemoryCostKib(int memoryCostKib) {
+      this.memoryCostKib = memoryCostKib;
+      return this;
+    }
+
+    /**
+     * Sets the version of the Argon2 algorithm.
+     *
+     * @param version a value from the {@link Argon2Version} enum.
+     * @return This builder.
+     */
+    public Builder setVersion(Argon2Version version) {
+      this.version = version;
+      return this;
+    }
+
+    /**
+     * Sets additional associated data, if provided, to append to the hash value for additional
+     * security. This data is base64 encoded before it is sent to the API.
+     *
+     * @param associatedData Associated data as a byte array.
+     * @return This builder.
+     */
+    public Builder setAssociatedData(byte[] associatedData) {
+      this.associatedData = associatedData;
+      return this;
+    }
+
+    public Argon2 build() {
+      return new Argon2(this);
+    }
+  }
+
+  public enum Argon2HashType {
+    ARGON2_D,
+    ARGON2_ID,
+    ARGON2_I
+  }
+
+  public enum Argon2Version {
+    VERSION_10,
+    VERSION_13
+  }
+}


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/UserImportHashTest.java b/src/test/java/com/google/firebase/auth/UserImportHashTest.java
index e4a3af1..95d8508 100644
--- a/src/test/java/com/google/firebase/auth/UserImportHashTest.java
+++ b/src/test/java/com/google/firebase/auth/UserImportHashTest.java
@@ -20,6 +20,9 @@ import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.BaseEncoding;
+import com.google.firebase.auth.hash.Argon2;
+import com.google.firebase.auth.hash.Argon2.Argon2HashType;
+import com.google.firebase.auth.hash.Argon2.Argon2Version;
 import com.google.firebase.auth.hash.Bcrypt;
 import com.google.firebase.auth.hash.HmacMd5;
 import com.google.firebase.auth.hash.HmacSha1;
@@ -40,6 +43,7 @@ public class UserImportHashTest {
 
   private static final byte[] SIGNER_KEY = "key".getBytes();
   private static final byte[] SALT_SEPARATOR = "separator".getBytes();
+  private static final byte[] ARGON2_ASSOCIATED_DATA = "associatedData".getBytes();
 
   private static class MockHash extends UserImportHash {
     MockHash() {
@@ -109,6 +113,94 @@ public class UserImportHashTest {
     assertEquals(properties, scrypt.getProperties());
   }
 
+  @Test
+  public void testArgon2Hash_withoutAssociatedDataWithVersion() {
+    UserImportHash argon2 = Argon2.builder()
+        .setHashLengthBytes(512)
+        .setHashType(Argon2HashType.ARGON2_ID)
+        .setParallelism(8)
+        .setIterations(16)
+        .setMemoryCostKib(512)
+        .setVersion(Argon2Version.VERSION_10)
+        .build();
+
+    Map<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
+        .put("hashLengthBytes", 512)
+        .put("hashType", "ARGON2_ID")
+        .put("parallelism", 8)
+        .put("iterations", 16)
+        .put("memoryCostKib", 512)
+        .put("version", "VERSION_10")
+        .build();
+    assertEquals(getArgon2ParametersMap(argon2Parameters), argon2.getProperties());
+  }
+
+  @Test
+  public void testArgon2Hash_withAssociatedDataWithoutVersion() {
+    UserImportHash argon2 = Argon2.builder()
+        .setHashLengthBytes(512)
+        .setHashType(Argon2HashType.ARGON2_ID)
+        .setParallelism(8)
+        .setIterations(16)
+        .setMemoryCostKib(512)
+        .setAssociatedData(ARGON2_ASSOCIATED_DATA)
+        .build();
+
+    Map<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
+        .put("hashLengthBytes", 512)
+        .put("hashType", "ARGON2_ID")
+        .put("parallelism", 8)
+        .put("iterations", 16)
+        .put("memoryCostKib", 512)
+        .put("associatedData", BaseEncoding.base64Url().encode(ARGON2_ASSOCIATED_DATA))
+        .build();
+    assertEquals(getArgon2ParametersMap(argon2Parameters), argon2.getProperties());
+  }
+
+  @Test
+  public void testArgon2Hash_withAssociatedDataAndVersion() {
+    UserImportHash argon2 = Argon2.builder()
+        .setHashLengthBytes(512)
+        .setHashType(Argon2HashType.ARGON2_ID)
+        .setParallelism(8)
+        .setIterations(16)
+        .setMemoryCostKib(512)
+        .setAssociatedData(ARGON2_ASSOCIATED_DATA)
+        .setVersion(Argon2Version.VERSION_10)
+        .build();
+
+    Map<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
+        .put("hashLengthBytes", 512)
+        .put("hashType", "ARGON2_ID")
+        .put("parallelism", 8)
+        .put("iterations", 16)
+        .put("memoryCostKib", 512)
+        .put("associatedData", BaseEncoding.base64Url().encode(ARGON2_ASSOCIATED_DATA))
+        .put("version", "VERSION_10")
+        .build();
+    assertEquals(getArgon2ParametersMap(argon2Parameters), argon2.getProperties());
+  }
+
+  @Test
+  public void testArgon2Hash_withoutAssociatedDataAndVersion() {
+    UserImportHash argon2 = Argon2.builder()
+        .setHashLengthBytes(512)
+        .setHashType(Argon2HashType.ARGON2_ID)
+        .setParallelism(8)
+        .setIterations(16)
+        .setMemoryCostKib(2048)
+        .build();
+
+    Map<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
+        .put("hashLengthBytes", 512)
+        .put("hashType", "ARGON2_ID")
+        .put("parallelism", 8)
+        .put("iterations", 16)
+        .put("memoryCostKib", 2048)
+        .build();
+    assertEquals(getArgon2ParametersMap(argon2Parameters), argon2.getProperties());
+  }
+
   @Test
   public void testHmacHash() {
     Map<String, UserImportHash> hashes = ImmutableMap.<String, UserImportHash>of(
@@ -151,4 +243,11 @@ public class UserImportHashTest {
     Map<String, Object> properties = ImmutableMap.<String, Object>of("hashAlgorithm", "BCRYPT");
     assertEquals(properties, bcrypt.getProperties());
   }
+
+  private static ImmutableMap<String, Object> getArgon2ParametersMap(
+      Map<String, Object> argon2Parameters) {
+    return ImmutableMap.of(
+        "hashAlgorithm", "ARGON2",
+        "argon2Parameters", argon2Parameters);
+  }
 }
diff --git a/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java b/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java
index 5186a44..8087b85 100644
--- a/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java
+++ b/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java
@@ -20,6 +20,7 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
+import com.google.firebase.auth.hash.Argon2.Argon2HashType;
 import java.util.List;
 import org.junit.Test;
 
@@ -124,4 +125,71 @@ public class InvalidHashTest {
       }
     }
   }
+
+  @Test
+  public void testInvalidArgon2() {
+    List<Argon2.Builder> builders = ImmutableList.of(
+        Argon2.builder() // hashLengthBytes < 4
+            .setHashLengthBytes(2)
+            .setHashType(Argon2HashType.ARGON2_ID)
+            .setParallelism(8)
+            .setIterations(16)
+            .setMemoryCostKib(512),
+        Argon2.builder() // hashLengthBytes > 1024
+            .setHashLengthBytes(2048)
+            .setHashType(Argon2HashType.ARGON2_ID)
+            .setParallelism(8)
+            .setIterations(16)
+            .setMemoryCostKib(512),
+        Argon2.builder() // missing hashType
+            .setHashLengthBytes(32)
+            .setParallelism(8)
+            .setIterations(16)
+            .setMemoryCostKib(512),
+        Argon2.builder() // parallelism < 1
+            .setHashType(Argon2HashType.ARGON2_ID)
+            .setParallelism(0)
+            .setHashLengthBytes(32)
+            .setIterations(16)
+            .setMemoryCostKib(512),
+        Argon2.builder() // parallelism > 16
+            .setHashType(Argon2HashType.ARGON2_ID)
+            .setParallelism(32)
+            .setHashLengthBytes(32)
+            .setIterations(16)
+            .setMemoryCostKib(512),
+        Argon2.builder() // iterations < 1
+            .setHashType(Argon2HashType.ARGON2_ID)
+            .setParallelism(16)
+            .setHashLengthBytes(32)
+            .setIterations(0)
+            .setMemoryCostKib(512),
+        Argon2.builder() // iterations > 16
+            .setHashType(Argon2HashType.ARGON2_ID)
+            .setParallelism(16)
+            .setHashLengthBytes(32)
+            .setIterations(32)
+            .setMemoryCostKib(512),
+        Argon2.builder() // memoryCostKib < 1
+            .setHashType(Argon2HashType.ARGON2_ID)
+            .setParallelism(16)
+            .setHashLengthBytes(32)
+            .setIterations(8)
+            .setMemoryCostKib(0),
+        Argon2.builder() // memoryCostKib > 32768
+            .setHashType(Argon2HashType.ARGON2_ID)
+            .setParallelism(16)
+            .setHashLengthBytes(32)
+            .setIterations(8)
+            .setMemoryCostKib(32769)
+    );
+    for (Argon2.Builder builder : builders) {
+      try {
+        builder.build();
+        fail("No error thrown for invalid configuration");
+      } catch (IllegalArgumentException expected) {
+        // expected
+      }
+    }
+  }
 }