Skip to content

Commit 4ed5a07

Browse files
Adds Argon2 support for password hashing (#5441)
Signed-off-by: Aiden Lindsay <aiden.o.lindsay@gmail.com> Signed-off-by: Aiden Lindsay <87037572+aidenlindsay@users.noreply.github.com> Co-authored-by: Darshit Chanpura <dchanp@amazon.com>
1 parent 8590712 commit 4ed5a07

File tree

16 files changed

+900
-59
lines changed

16 files changed

+900
-59
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99

1010
* Introduced new experimental versioned security configuration management feature ([#5357] (https://github.com/opensearch-project/security/pull/5357))
1111
* [Resource Sharing] Adds migrate API to move resource-sharing info to security plugin ([#5389](https://github.com/opensearch-project/security/pull/5389))
12+
* Introduces support for the Argon2 Password Hashing Algorithm ([#5441] (https://github.com/opensearch-project/security/pull/5441))
1213

1314
### Enhancements
1415

@@ -42,5 +43,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4243

4344
### Documentation
4445

45-
4646
[Unreleased 3.x]: https://github.com/opensearch-project/security/compare/3.1...main
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*
8+
* Modifications Copyright OpenSearch Contributors. See
9+
* GitHub history for details.
10+
*/
11+
12+
package org.opensearch.security.hash;
13+
14+
import java.util.List;
15+
import java.util.Map;
16+
17+
import org.apache.http.HttpStatus;
18+
import org.awaitility.Awaitility;
19+
import org.junit.BeforeClass;
20+
import org.junit.Test;
21+
22+
import org.opensearch.security.support.ConfigConstants;
23+
import org.opensearch.test.framework.TestSecurityConfig;
24+
import org.opensearch.test.framework.cluster.ClusterManager;
25+
import org.opensearch.test.framework.cluster.LocalCluster;
26+
import org.opensearch.test.framework.cluster.TestRestClient;
27+
28+
import static org.hamcrest.Matchers.equalTo;
29+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
30+
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
31+
32+
public class Argon2CustomConfigHashingTests extends HashingTests {
33+
34+
public static LocalCluster cluster;
35+
36+
private static final String PASSWORD = "top$ecret1234!";
37+
38+
private static String type;
39+
private static int memory, iterations, parallelism, length, version;
40+
41+
@BeforeClass
42+
public static void startCluster() {
43+
44+
type = randomFrom(List.of("argon2id", "argon2i", "argon2d"));
45+
iterations = randomFrom(List.of(2, 3, 4));
46+
memory = randomFrom(List.of(65536, 131072));
47+
parallelism = randomFrom(List.of(1, 2));
48+
length = randomFrom(List.of(16, 32, 64));
49+
version = randomFrom(List.of(16, 19));
50+
51+
TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
52+
.hash(generateArgon2Hash("secret", memory, iterations, parallelism, length, type, version));
53+
cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
54+
.authc(AUTHC_HTTPBASIC_INTERNAL)
55+
.users(ADMIN_USER)
56+
.anonymousAuth(false)
57+
.nodeSettings(
58+
Map.of(
59+
ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED,
60+
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
61+
ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM,
62+
ConfigConstants.ARGON2,
63+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY,
64+
memory,
65+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS,
66+
iterations,
67+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM,
68+
parallelism,
69+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH,
70+
length,
71+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE,
72+
type,
73+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION,
74+
version
75+
)
76+
)
77+
.build();
78+
cluster.before();
79+
80+
try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) {
81+
Awaitility.await()
82+
.alias("Load default configuration")
83+
.until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP"));
84+
}
85+
}
86+
87+
@Test
88+
public void shouldAuthenticateWithCorrectPassword() {
89+
String hash = generateArgon2Hash(PASSWORD, memory, iterations, parallelism, length, type, version);
90+
createUserWithHashedPassword(cluster, "user_1", hash);
91+
testPasswordAuth(cluster, "user_1", PASSWORD, HttpStatus.SC_OK);
92+
93+
createUserWithPlainTextPassword(cluster, "user_2", PASSWORD);
94+
testPasswordAuth(cluster, "user_2", PASSWORD, HttpStatus.SC_OK);
95+
}
96+
97+
@Test
98+
public void shouldNotAuthenticateWithIncorrectPassword() {
99+
String hash = generateArgon2Hash(PASSWORD, memory, iterations, parallelism, length, type, version);
100+
createUserWithHashedPassword(cluster, "user_3", hash);
101+
testPasswordAuth(cluster, "user_3", "wrong_password", HttpStatus.SC_UNAUTHORIZED);
102+
103+
createUserWithPlainTextPassword(cluster, "user_4", PASSWORD);
104+
testPasswordAuth(cluster, "user_4", "wrong_password", HttpStatus.SC_UNAUTHORIZED);
105+
}
106+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*
8+
* Modifications Copyright OpenSearch Contributors. See
9+
* GitHub history for details.
10+
*/
11+
12+
package org.opensearch.security.hash;
13+
14+
import java.util.List;
15+
import java.util.Map;
16+
17+
import org.apache.http.HttpStatus;
18+
import org.junit.ClassRule;
19+
import org.junit.Test;
20+
21+
import org.opensearch.security.support.ConfigConstants;
22+
import org.opensearch.test.framework.TestSecurityConfig;
23+
import org.opensearch.test.framework.cluster.ClusterManager;
24+
import org.opensearch.test.framework.cluster.LocalCluster;
25+
26+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
27+
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
28+
29+
public class Argon2DefaultConfigHashingTests extends HashingTests {
30+
31+
private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
32+
.hash(
33+
generateArgon2Hash(
34+
"secret",
35+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
36+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
37+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
38+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
39+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
40+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT
41+
)
42+
);
43+
44+
@ClassRule
45+
public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
46+
.authc(AUTHC_HTTPBASIC_INTERNAL)
47+
.users(ADMIN_USER)
48+
.anonymousAuth(false)
49+
.nodeSettings(
50+
Map.of(
51+
ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED,
52+
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
53+
ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM,
54+
ConfigConstants.ARGON2
55+
)
56+
)
57+
.build();
58+
59+
@Test
60+
public void shouldAuthenticateWithCorrectPassword() {
61+
String hash = generateArgon2Hash(
62+
PASSWORD,
63+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
64+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
65+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
66+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
67+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
68+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT
69+
);
70+
createUserWithHashedPassword(cluster, "user_1", hash);
71+
testPasswordAuth(cluster, "user_1", PASSWORD, HttpStatus.SC_OK);
72+
73+
createUserWithPlainTextPassword(cluster, "user_2", PASSWORD);
74+
testPasswordAuth(cluster, "user_2", PASSWORD, HttpStatus.SC_OK);
75+
}
76+
77+
@Test
78+
public void shouldNotAuthenticateWithIncorrectPassword() {
79+
String hash = generateArgon2Hash(
80+
PASSWORD,
81+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
82+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
83+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
84+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
85+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
86+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT
87+
);
88+
createUserWithHashedPassword(cluster, "user_3", hash);
89+
testPasswordAuth(cluster, "user_3", "wrongpassword", HttpStatus.SC_UNAUTHORIZED);
90+
91+
createUserWithPlainTextPassword(cluster, "user_4", PASSWORD);
92+
testPasswordAuth(cluster, "user_4", "wrongpassword", HttpStatus.SC_UNAUTHORIZED);
93+
}
94+
}

src/integrationTest/java/org/opensearch/security/hash/HashingTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
import org.opensearch.test.framework.cluster.LocalCluster;
2323
import org.opensearch.test.framework.cluster.TestRestClient;
2424

25+
import com.password4j.Argon2Function;
2526
import com.password4j.BcryptFunction;
2627
import com.password4j.CompressedPBKDF2Function;
2728
import com.password4j.Password;
29+
import com.password4j.types.Argon2;
2830
import com.password4j.types.Bcrypt;
2931

3032
import static org.hamcrest.MatcherAssert.assertThat;
@@ -78,4 +80,23 @@ public static String generatePBKDF2Hash(String password, String algorithm, int i
7880
.getResult();
7981
}
8082

83+
public static String generateArgon2Hash(
84+
String password,
85+
int memory,
86+
int iterations,
87+
int parallelism,
88+
int length,
89+
String type,
90+
int version
91+
) {
92+
Argon2 argon2Type = switch (type.toUpperCase()) {
93+
case "ARGON2ID" -> Argon2.ID;
94+
case "ARGON2I" -> Argon2.I;
95+
case "ARGON2D" -> Argon2.D;
96+
default -> throw new IllegalArgumentException("Unknown Argon2 type: " + type);
97+
};
98+
return Password.hash(CharBuffer.wrap(password.toCharArray()))
99+
.with(Argon2Function.getInstance(memory, iterations, parallelism, length, argon2Type, version))
100+
.getResult();
101+
}
81102
}

src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,55 @@ public List<Setting<?>> getSettings() {
14371437
)
14381438
);
14391439

1440+
settings.add(
1441+
Setting.intSetting(
1442+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS,
1443+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
1444+
Property.NodeScope,
1445+
Property.Final
1446+
)
1447+
);
1448+
settings.add(
1449+
Setting.intSetting(
1450+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY,
1451+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
1452+
Property.NodeScope,
1453+
Property.Final
1454+
)
1455+
);
1456+
settings.add(
1457+
Setting.intSetting(
1458+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM,
1459+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
1460+
Property.NodeScope,
1461+
Property.Final
1462+
)
1463+
);
1464+
settings.add(
1465+
Setting.intSetting(
1466+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH,
1467+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
1468+
Property.NodeScope,
1469+
Property.Final
1470+
)
1471+
);
1472+
settings.add(
1473+
Setting.simpleString(
1474+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE,
1475+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
1476+
Property.NodeScope,
1477+
Property.Final
1478+
)
1479+
);
1480+
settings.add(
1481+
Setting.intSetting(
1482+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION,
1483+
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT,
1484+
Property.NodeScope,
1485+
Property.Final
1486+
)
1487+
);
1488+
14401489
if (!SSLConfig.isSslOnlyMode()) {
14411490
settings.add(
14421491
Setting.listSetting(

src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ public final class AuditMessage {
7575
@VisibleForTesting
7676
public static final String BCRYPT_REGEX = "\\$2[ayb]\\$.{56}";
7777
public static final String PBKDF2_REGEX = "\\$\\d+\\$\\d+\\$[A-Za-z0-9+/]+={0,2}\\$[A-Za-z0-9+/]+={0,2}";
78-
public static final Pattern HASH_REGEX_PATTERN = Pattern.compile(BCRYPT_REGEX + "|" + PBKDF2_REGEX);
78+
public static final String ARGON2_REGEX =
79+
"\\$argon2(?:id|i|d)\\$v=\\d+\\$(?:[a-z]=\\d+,?)+\\$[A-Za-z0-9+/]+={0,2}\\$[A-Za-z0-9+/]+={0,2}";
80+
public static final Pattern HASH_REGEX_PATTERN = Pattern.compile(BCRYPT_REGEX + "|" + PBKDF2_REGEX + "|" + ARGON2_REGEX);
7981
private static final String HASH_REPLACEMENT_VALUE = "__HASH__";
8082
private static final String INTERNALUSERS_DOC_ID = CType.INTERNALUSERS.toLCString();
8183

0 commit comments

Comments
 (0)