Skip to content

Commit 148fc1b

Browse files
authored
Merge pull request #4857 from gchq/datafeed-keys-add-bcrypt
Add BCrupt hasher, change the way datafeed key auth works
2 parents 70a1c23 + bd9d5c5 commit 148fc1b

19 files changed

+533
-192
lines changed

gradle/libs.versions.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ aws-crt = { module = "software.amazon.awssdk.crt:aws-crt", version = "0.29.22" }
3636
aws-s3-transfer-manager = { module = "software.amazon.awssdk:s3-transfer-manager" } # version controlled by AWS BOM
3737
aws-sqs = { module = "software.amazon.awssdk:sqs" } # version controlled by AWS BOM
3838
aws-sts = { module = "software.amazon.awssdk:sts" } # version controlled by AWS BOM
39-
bcrypt = { module = "de.svenkubiak:jBCrypt", version = "0.4.1" }
39+
bcrypt = { module = "de.svenkubiak:jBCrypt", version = "0.4.3" }
4040
bouncy-castle = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.78.1" }
4141
caffeine = { module = "com.github.ben-manes.caffeine:caffeine" } # version controlled by dropwizard-dependencies
4242
commons-beanutils = { module = "commons-beanutils:commons-beanutils", version = "1.9.4" }

stroom-receive/stroom-receive-common/src/main/java/stroom/receive/common/Argon2DataFeedKeyHasher.java

+31-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package stroom.receive.common;
22

3-
import stroom.util.string.Base58;
3+
import stroom.util.string.StringUtil;
44

5+
import org.apache.commons.codec.binary.Hex;
56
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
67
import org.bouncycastle.crypto.params.Argon2Parameters;
78
import org.bouncycastle.crypto.params.Argon2Parameters.Builder;
89

910
import java.nio.charset.StandardCharsets;
11+
import java.security.SecureRandom;
1012
import java.util.Objects;
1113

1214
class Argon2DataFeedKeyHasher implements DataFeedKeyHasher {
@@ -20,23 +22,33 @@ class Argon2DataFeedKeyHasher implements DataFeedKeyHasher {
2022
private static final int MEMORY_KB = 65_536;
2123
private static final int PARALLELISM = 1;
2224

23-
private final Argon2Parameters argon2Parameters;
25+
private final SecureRandom secureRandom;
2426

2527
public Argon2DataFeedKeyHasher() {
26-
// No salt given the length of api keys being hashed
27-
this.argon2Parameters = new Builder(Argon2Parameters.ARGON2_id)
28+
this.secureRandom = new SecureRandom();
29+
}
30+
31+
@Override
32+
public HashOutput hash(final String dataFeedKey) {
33+
final String randomSalt = StringUtil.createRandomCode(
34+
secureRandom,
35+
32,
36+
StringUtil.ALLOWED_CHARS_BASE_58_STYLE);
37+
return hash(dataFeedKey, randomSalt);
38+
}
39+
40+
public HashOutput hash(final String dataFeedKey, final String salt) {
41+
final Argon2Parameters params = new Builder(Argon2Parameters.ARGON2_id)
2842
.withVersion(Argon2Parameters.ARGON2_VERSION_13)
2943
.withIterations(ITERATIONS)
3044
.withMemoryAsKB(MEMORY_KB)
3145
.withParallelism(PARALLELISM)
46+
.withSalt(salt.getBytes(StandardCharsets.UTF_8))
3247
.build();
33-
}
3448

35-
@Override
36-
public String hash(final String dataFeedKey) {
3749
Objects.requireNonNull(dataFeedKey);
38-
Argon2BytesGenerator generator = new Argon2BytesGenerator();
39-
generator.init(argon2Parameters);
50+
final Argon2BytesGenerator generator = new Argon2BytesGenerator();
51+
generator.init(params);
4052
byte[] result = new byte[HASH_LENGTH];
4153
generator.generateBytes(
4254
dataFeedKey.trim().getBytes(StandardCharsets.UTF_8),
@@ -46,7 +58,16 @@ public String hash(final String dataFeedKey) {
4658

4759
// Base58 is a bit less nasty than base64 and widely supported in other languages
4860
// due to use in bitcoin.
49-
return Base58.encode(result);
61+
final String hash = Hex.encodeHexString(result);
62+
return new HashOutput(hash, salt);
63+
}
64+
65+
@Override
66+
public boolean verify(final String dataFeedKey, final String hash, final String salt) {
67+
Objects.requireNonNull(dataFeedKey);
68+
Objects.requireNonNull(hash);
69+
final HashOutput hashOutput = hash(dataFeedKey, salt);
70+
return Objects.equals(hash, hashOutput.hash());
5071
}
5172

5273
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package stroom.receive.common;
2+
3+
import stroom.util.shared.NullSafe;
4+
5+
import org.mindrot.jbcrypt.BCrypt;
6+
7+
import java.security.SecureRandom;
8+
import java.util.Objects;
9+
10+
public class BCryptDataFeedKeyHasher implements DataFeedKeyHasher {
11+
12+
private final SecureRandom secureRandom = new SecureRandom();
13+
14+
@Override
15+
public HashOutput hash(final String dataFeedKey) {
16+
final String generatedSalt = BCrypt.gensalt(10, secureRandom);
17+
final String hash = BCrypt.hashpw(Objects.requireNonNull(dataFeedKey), generatedSalt);
18+
return new HashOutput(hash, generatedSalt);
19+
}
20+
21+
@Override
22+
public boolean verify(final String dataFeedKey, final String hash, final String salt) {
23+
if (NullSafe.isEmptyString(dataFeedKey)) {
24+
return false;
25+
} else {
26+
return BCrypt.checkpw(dataFeedKey, hash);
27+
}
28+
}
29+
30+
@Override
31+
public DataFeedKeyHashAlgorithm getAlgorithm() {
32+
return DataFeedKeyHashAlgorithm.BCRYPT_2A;
33+
}
34+
}

stroom-receive/stroom-receive-common/src/main/java/stroom/receive/common/CachedHashedDataFeedKey.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,23 @@ public Path getSourceFile() {
3737

3838
/**
3939
* @return The hash of the data feed key. The hash algorithm used is defined by
40-
* {@link CachedHashedDataFeedKey#getHashAlgorithmId()}
40+
* {@link CachedHashedDataFeedKey#getHashAlgorithm()}
4141
*/
4242
@NotBlank
4343
public String getHash() {
4444
return hashedDataFeedKey.getHash();
4545
}
4646

47+
/**
48+
* @return May be null if the algorithm encodes the salt in the hash
49+
*/
50+
public String getSalt() {
51+
return hashedDataFeedKey.getSalt();
52+
}
53+
4754
@NotBlank
48-
public String getHashAlgorithmId() {
49-
return hashedDataFeedKey.getHashAlgorithmId();
55+
public DataFeedKeyHashAlgorithm getHashAlgorithm() {
56+
return hashedDataFeedKey.getHashAlgorithm();
5057
}
5158

5259
/**

stroom-receive/stroom-receive-common/src/main/java/stroom/receive/common/DataFeedKeyHashAlgorithm.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
public enum DataFeedKeyHashAlgorithm implements HasDisplayValue {
1414
// uniqueIds should be
1515
ARGON2("Argon2", 0),
16-
// BCRYPT("BCrypt", 0),
16+
BCRYPT_2A("BCrypt (2a)", 1),
1717
;
1818

1919
private static final DataFeedKeyHashAlgorithm[] SPARSE_ALGORITHM_ARRAY;
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,40 @@
11
package stroom.receive.common;
22

3+
import java.util.Objects;
4+
35
interface DataFeedKeyHasher {
46

5-
String hash(String dataFeedKey);
7+
/**
8+
* Generate the hash of a datafeed key.
9+
*
10+
* @param dataFeedKey The datafeed key to generate a hash for
11+
* @return The hash and the salt used.
12+
*/
13+
HashOutput hash(String dataFeedKey);
614

7-
// default boolean verify(String apiKeyStr, String hash) {
8-
// final String computedHash = hash(Objects.requireNonNull(apiKeyStr));
9-
// return Objects.equals(Objects.requireNonNull(hash), computedHash);
10-
// }
15+
/**
16+
* Verify a dataFeedKey against its hash, and if provided include its salt.
17+
*
18+
* @param dataFeedKey The datafeed key
19+
* @param hash The hash to verify against.
20+
* @param salt An optional salt to include in the verification
21+
* @return True if verification is successful
22+
*/
23+
default boolean verify(String dataFeedKey, String hash, String salt) {
24+
final HashOutput hashOutput = hash(Objects.requireNonNull(dataFeedKey));
25+
return Objects.equals(Objects.requireNonNull(hash), hashOutput.hash);
26+
}
1127

28+
/**
29+
* @return The enum representing this hash algorithm.
30+
*/
1231
DataFeedKeyHashAlgorithm getAlgorithm();
32+
33+
34+
// --------------------------------------------------------------------------------
35+
36+
37+
record HashOutput(String hash, String salt) {
38+
39+
}
1340
}

0 commit comments

Comments
 (0)