Skip to content

Commit 33b9033

Browse files
authored
Add Argon2 Hashing Algorithm support (#637)
* Add Argon2 * More checks for associatedData * Use String for associated data, add another test * Removed default handling for Version in Argon2 * lint fix * more tests * Address comments. Use consts for param limits * Add javadoc for the Builder methods * Fix the mapping for Argon2. All parameters must be inside the `argon2Parameters` field
1 parent 8168b17 commit 33b9033

File tree

3 files changed

+384
-0
lines changed

3 files changed

+384
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* Copyright 2022 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.auth.hash;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
21+
import com.google.common.collect.ImmutableMap;
22+
import com.google.common.io.BaseEncoding;
23+
import com.google.firebase.auth.UserImportHash;
24+
import java.util.Map;
25+
26+
/**
27+
* Represents the Argon2 password hashing algorithm. Can be used as an instance of {@link
28+
* com.google.firebase.auth.UserImportHash} when importing users.
29+
*/
30+
public final class Argon2 extends UserImportHash {
31+
32+
private static final int MIN_HASH_LENGTH_BYTES = 4;
33+
private static final int MAX_HASH_LENGTH_BYTES = 1024;
34+
private static final int MIN_PARALLELISM = 1;
35+
private static final int MAX_PARALLELISM = 16;
36+
private static final int MIN_ITERATIONS = 1;
37+
private static final int MAX_ITERATIONS = 16;
38+
private static final int MIN_MEMORY_COST_KIB = 1;
39+
private static final int MAX_MEMORY_COST_KIB = 32768;
40+
41+
private final int hashLengthBytes;
42+
private final Argon2HashType hashType;
43+
private final int parallelism;
44+
private final int iterations;
45+
private final int memoryCostKib;
46+
private final Argon2Version version;
47+
private final String associatedData;
48+
49+
private Argon2(Builder builder) {
50+
super("ARGON2");
51+
checkArgument(intShouldBeBetweenLimitsInclusive(builder.hashLengthBytes, MIN_HASH_LENGTH_BYTES,
52+
MAX_HASH_LENGTH_BYTES),
53+
"hashLengthBytes is required for Argon2 and must be between %s and %s",
54+
MIN_HASH_LENGTH_BYTES, MAX_HASH_LENGTH_BYTES);
55+
checkArgument(builder.hashType != null,
56+
"A hashType is required for Argon2");
57+
checkArgument(
58+
intShouldBeBetweenLimitsInclusive(builder.parallelism, MIN_PARALLELISM, MAX_PARALLELISM),
59+
"parallelism is required for Argon2 and must be between %s and %s", MIN_PARALLELISM,
60+
MAX_PARALLELISM);
61+
checkArgument(
62+
intShouldBeBetweenLimitsInclusive(builder.iterations, MIN_ITERATIONS, MAX_ITERATIONS),
63+
"iterations is required for Argon2 and must be between %s and %s", MIN_ITERATIONS,
64+
MAX_ITERATIONS);
65+
checkArgument(intShouldBeBetweenLimitsInclusive(builder.memoryCostKib, MIN_MEMORY_COST_KIB,
66+
MAX_MEMORY_COST_KIB),
67+
"memoryCostKib is required for Argon2 and must be less than or equal to %s",
68+
MAX_MEMORY_COST_KIB);
69+
this.hashLengthBytes = builder.hashLengthBytes;
70+
this.hashType = builder.hashType;
71+
this.parallelism = builder.parallelism;
72+
this.iterations = builder.iterations;
73+
this.memoryCostKib = builder.memoryCostKib;
74+
if (builder.version != null) {
75+
this.version = builder.version;
76+
} else {
77+
this.version = null;
78+
}
79+
if (builder.associatedData != null) {
80+
this.associatedData = BaseEncoding.base64Url().encode(builder.associatedData);
81+
} else {
82+
this.associatedData = null;
83+
}
84+
}
85+
86+
private static boolean intShouldBeBetweenLimitsInclusive(int property, int fromInclusive,
87+
int toInclusive) {
88+
return property >= fromInclusive && property <= toInclusive;
89+
}
90+
91+
@Override
92+
protected Map<String, Object> getOptions() {
93+
ImmutableMap.Builder<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
94+
.put("hashLengthBytes", hashLengthBytes)
95+
.put("hashType", hashType.toString())
96+
.put("parallelism", parallelism)
97+
.put("iterations", iterations)
98+
.put("memoryCostKib", memoryCostKib);
99+
if (this.associatedData != null) {
100+
argon2Parameters.put("associatedData", associatedData);
101+
}
102+
if (this.version != null) {
103+
argon2Parameters.put("version", version.toString());
104+
}
105+
return ImmutableMap.<String, Object>of("argon2Parameters", argon2Parameters.build());
106+
}
107+
108+
public static Builder builder() {
109+
return new Builder();
110+
}
111+
112+
public static class Builder {
113+
114+
private int hashLengthBytes;
115+
private Argon2HashType hashType;
116+
private int parallelism;
117+
private int iterations;
118+
private int memoryCostKib;
119+
private Argon2Version version;
120+
private byte[] associatedData;
121+
122+
private Builder() {}
123+
124+
/**
125+
* Sets the hash length in bytes. Required field.
126+
*
127+
* @param hashLengthBytes an integer between 4 and 1024 (inclusive).
128+
* @return This builder.
129+
*/
130+
public Builder setHashLengthBytes(int hashLengthBytes) {
131+
this.hashLengthBytes = hashLengthBytes;
132+
return this;
133+
}
134+
135+
/**
136+
* Sets the Argon2 hash type. Required field.
137+
*
138+
* @param hashType a value from the {@link Argon2HashType} enum.
139+
* @return This builder.
140+
*/
141+
public Builder setHashType(Argon2HashType hashType) {
142+
this.hashType = hashType;
143+
return this;
144+
}
145+
146+
/**
147+
* Sets the degree of parallelism, also called threads or lanes. Required field.
148+
*
149+
* @param parallelism an integer between 1 and 16 (inclusive).
150+
* @return This builder.
151+
*/
152+
public Builder setParallelism(int parallelism) {
153+
this.parallelism = parallelism;
154+
return this;
155+
}
156+
157+
/**
158+
* Sets the number of iterations to perform. Required field.
159+
*
160+
* @param iterations an integer between 1 and 16 (inclusive).
161+
* @return This builder.
162+
*/
163+
public Builder setIterations(int iterations) {
164+
this.iterations = iterations;
165+
return this;
166+
}
167+
168+
/**
169+
* Sets the memory cost in kibibytes. Required field.
170+
*
171+
* @param memoryCostKib an integer between 1 and 32768 (inclusive).
172+
* @return This builder.
173+
*/
174+
public Builder setMemoryCostKib(int memoryCostKib) {
175+
this.memoryCostKib = memoryCostKib;
176+
return this;
177+
}
178+
179+
/**
180+
* Sets the version of the Argon2 algorithm.
181+
*
182+
* @param version a value from the {@link Argon2Version} enum.
183+
* @return This builder.
184+
*/
185+
public Builder setVersion(Argon2Version version) {
186+
this.version = version;
187+
return this;
188+
}
189+
190+
/**
191+
* Sets additional associated data, if provided, to append to the hash value for additional
192+
* security. This data is base64 encoded before it is sent to the API.
193+
*
194+
* @param associatedData Associated data as a byte array.
195+
* @return This builder.
196+
*/
197+
public Builder setAssociatedData(byte[] associatedData) {
198+
this.associatedData = associatedData;
199+
return this;
200+
}
201+
202+
public Argon2 build() {
203+
return new Argon2(this);
204+
}
205+
}
206+
207+
public enum Argon2HashType {
208+
ARGON2_D,
209+
ARGON2_ID,
210+
ARGON2_I
211+
}
212+
213+
public enum Argon2Version {
214+
VERSION_10,
215+
VERSION_13
216+
}
217+
}

src/test/java/com/google/firebase/auth/UserImportHashTest.java

+99
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020

2121
import com.google.common.collect.ImmutableMap;
2222
import com.google.common.io.BaseEncoding;
23+
import com.google.firebase.auth.hash.Argon2;
24+
import com.google.firebase.auth.hash.Argon2.Argon2HashType;
25+
import com.google.firebase.auth.hash.Argon2.Argon2Version;
2326
import com.google.firebase.auth.hash.Bcrypt;
2427
import com.google.firebase.auth.hash.HmacMd5;
2528
import com.google.firebase.auth.hash.HmacSha1;
@@ -40,6 +43,7 @@ public class UserImportHashTest {
4043

4144
private static final byte[] SIGNER_KEY = "key".getBytes();
4245
private static final byte[] SALT_SEPARATOR = "separator".getBytes();
46+
private static final byte[] ARGON2_ASSOCIATED_DATA = "associatedData".getBytes();
4347

4448
private static class MockHash extends UserImportHash {
4549
MockHash() {
@@ -109,6 +113,94 @@ public void testStandardScryptHash() {
109113
assertEquals(properties, scrypt.getProperties());
110114
}
111115

116+
@Test
117+
public void testArgon2Hash_withoutAssociatedDataWithVersion() {
118+
UserImportHash argon2 = Argon2.builder()
119+
.setHashLengthBytes(512)
120+
.setHashType(Argon2HashType.ARGON2_ID)
121+
.setParallelism(8)
122+
.setIterations(16)
123+
.setMemoryCostKib(512)
124+
.setVersion(Argon2Version.VERSION_10)
125+
.build();
126+
127+
Map<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
128+
.put("hashLengthBytes", 512)
129+
.put("hashType", "ARGON2_ID")
130+
.put("parallelism", 8)
131+
.put("iterations", 16)
132+
.put("memoryCostKib", 512)
133+
.put("version", "VERSION_10")
134+
.build();
135+
assertEquals(getArgon2ParametersMap(argon2Parameters), argon2.getProperties());
136+
}
137+
138+
@Test
139+
public void testArgon2Hash_withAssociatedDataWithoutVersion() {
140+
UserImportHash argon2 = Argon2.builder()
141+
.setHashLengthBytes(512)
142+
.setHashType(Argon2HashType.ARGON2_ID)
143+
.setParallelism(8)
144+
.setIterations(16)
145+
.setMemoryCostKib(512)
146+
.setAssociatedData(ARGON2_ASSOCIATED_DATA)
147+
.build();
148+
149+
Map<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
150+
.put("hashLengthBytes", 512)
151+
.put("hashType", "ARGON2_ID")
152+
.put("parallelism", 8)
153+
.put("iterations", 16)
154+
.put("memoryCostKib", 512)
155+
.put("associatedData", BaseEncoding.base64Url().encode(ARGON2_ASSOCIATED_DATA))
156+
.build();
157+
assertEquals(getArgon2ParametersMap(argon2Parameters), argon2.getProperties());
158+
}
159+
160+
@Test
161+
public void testArgon2Hash_withAssociatedDataAndVersion() {
162+
UserImportHash argon2 = Argon2.builder()
163+
.setHashLengthBytes(512)
164+
.setHashType(Argon2HashType.ARGON2_ID)
165+
.setParallelism(8)
166+
.setIterations(16)
167+
.setMemoryCostKib(512)
168+
.setAssociatedData(ARGON2_ASSOCIATED_DATA)
169+
.setVersion(Argon2Version.VERSION_10)
170+
.build();
171+
172+
Map<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
173+
.put("hashLengthBytes", 512)
174+
.put("hashType", "ARGON2_ID")
175+
.put("parallelism", 8)
176+
.put("iterations", 16)
177+
.put("memoryCostKib", 512)
178+
.put("associatedData", BaseEncoding.base64Url().encode(ARGON2_ASSOCIATED_DATA))
179+
.put("version", "VERSION_10")
180+
.build();
181+
assertEquals(getArgon2ParametersMap(argon2Parameters), argon2.getProperties());
182+
}
183+
184+
@Test
185+
public void testArgon2Hash_withoutAssociatedDataAndVersion() {
186+
UserImportHash argon2 = Argon2.builder()
187+
.setHashLengthBytes(512)
188+
.setHashType(Argon2HashType.ARGON2_ID)
189+
.setParallelism(8)
190+
.setIterations(16)
191+
.setMemoryCostKib(2048)
192+
.build();
193+
194+
Map<String, Object> argon2Parameters = ImmutableMap.<String, Object>builder()
195+
.put("hashLengthBytes", 512)
196+
.put("hashType", "ARGON2_ID")
197+
.put("parallelism", 8)
198+
.put("iterations", 16)
199+
.put("memoryCostKib", 2048)
200+
.build();
201+
assertEquals(getArgon2ParametersMap(argon2Parameters), argon2.getProperties());
202+
}
203+
112204
@Test
113205
public void testHmacHash() {
114206
Map<String, UserImportHash> hashes = ImmutableMap.<String, UserImportHash>of(
@@ -151,4 +243,11 @@ public void testBcryptHash() {
151243
Map<String, Object> properties = ImmutableMap.<String, Object>of("hashAlgorithm", "BCRYPT");
152244
assertEquals(properties, bcrypt.getProperties());
153245
}
246+
247+
private static ImmutableMap<String, Object> getArgon2ParametersMap(
248+
Map<String, Object> argon2Parameters) {
249+
return ImmutableMap.of(
250+
"hashAlgorithm", "ARGON2",
251+
"argon2Parameters", argon2Parameters);
252+
}
154253
}

0 commit comments

Comments
 (0)