Skip to content

feat(auth): Add Argon2 Hashing Algorithm support #637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions src/main/java/com/google/firebase/auth/hash/Argon2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* 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> builder = ImmutableMap.<String, Object>builder()
.put("hashLengthBytes", hashLengthBytes)
.put("hashType", hashType.toString())
.put("parallelism", parallelism)
.put("iterations", iterations)
.put("memoryCostKib", memoryCostKib);
if (this.associatedData != null) {
builder.put("associatedData", associatedData);
}
if (this.version != null) {
builder.put("version", version.toString());
}
return builder.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() {
}

public Builder setHashLengthBytes(int hashLengthBytes) {
this.hashLengthBytes = hashLengthBytes;
return this;
}

public Builder setHashType(Argon2HashType hashType) {
this.hashType = hashType;
return this;
}

public Builder setParallelism(int parallelism) {
this.parallelism = parallelism;
return this;
}

public Builder setIterations(int iterations) {
this.iterations = iterations;
return this;
}

public Builder setMemoryCostKib(int memoryCostKib) {
this.memoryCostKib = memoryCostKib;
return this;
}

public Builder setVersion(Argon2Version version) {
this.version = version;
return this;
}

public Builder setAssociatedData(byte[] associatedData) {
this.associatedData = associatedData;
return this;
}

public Argon2 build() {
return new Argon2(this);
}
}

public enum Argon2HashType {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this could use another layer of abstraction. Similar to what we have done with Hmac and RepeatableHash. We could introduce an abstract class for ARGON2Hash and implementations for ARGON2d, ARGON2i, and ARGON2id. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's unnecessary here. For HMAC, it makes sense to add a layer of abstraction since the algorithms are identified as different from one another (HMAC_SHA256 HMAC_SHA1 HMAC_MD5, etc). Here ARGON2 is the algorithm, the hash-types are an additional configuration for the same algorithm.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. That makes sense. Thanks, Sam!

ARGON2_D,
ARGON2_ID,
ARGON2_I
}

public enum Argon2Version {
VERSION_10,
VERSION_13
Comment on lines +214 to +215

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these 2 specific versions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}
96 changes: 96 additions & 0 deletions src/test/java/com/google/firebase/auth/UserImportHashTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

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;
Expand All @@ -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() {
Expand Down Expand Up @@ -109,6 +113,98 @@ public void testStandardScryptHash() {
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> properties = ImmutableMap.<String, Object>builder()
.put("hashAlgorithm", "ARGON2")
.put("hashLengthBytes", 512)
.put("hashType", "ARGON2_ID")
.put("parallelism", 8)
.put("iterations", 16)
.put("memoryCostKib", 512)
.put("version", "VERSION_10")
.build();
assertEquals(properties, 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> properties = ImmutableMap.<String, Object>builder()
.put("hashAlgorithm", "ARGON2")
.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(properties, 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> properties = ImmutableMap.<String, Object>builder()
.put("hashAlgorithm", "ARGON2")
.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(properties, 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> properties = ImmutableMap.<String, Object>builder()
.put("hashAlgorithm", "ARGON2")
.put("hashLengthBytes", 512)
.put("hashType", "ARGON2_ID")
.put("parallelism", 8)
.put("iterations", 16)
.put("memoryCostKib", 2048)
.build();
assertEquals(properties, argon2.getProperties());
}

@Test
public void testHmacHash() {
Map<String, UserImportHash> hashes = ImmutableMap.<String, UserImportHash>of(
Expand Down
68 changes: 68 additions & 0 deletions src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;

Expand Down Expand Up @@ -124,4 +125,71 @@ public void testInvalidScrypt() {
}
}
}

@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
}
}
}
}