Skip to content

Commit 2029526

Browse files
Dan CecoiDan Cecoi
Dan Cecoi
authored and
Dan Cecoi
committed
Add support for PBKDF2, Scrypt, and Argon2 for password hashing (opensearch-project#4079)
Signed-off-by: Dan Cecoi <[email protected]>
1 parent 3b94522 commit 2029526

31 files changed

+1938
-92
lines changed

build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,8 @@ dependencies {
583583
implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3'
584584
implementation 'com.rfksystems:blake2b:2.0.0'
585585

586+
implementation 'com.password4j:password4j:1.8.2'
587+
586588
//JWT
587589
implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}"
588590
implementation "io.jsonwebtoken:jjwt-impl:${jjwt_version}"

src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@
1010

1111
package org.opensearch.security.api;
1212

13+
import java.nio.CharBuffer;
14+
1315
import org.junit.Test;
1416

1517
import org.opensearch.core.xcontent.ToXContentObject;
18+
import org.opensearch.security.hash.PasswordHasher;
19+
import org.opensearch.security.hash.PasswordHasherImpl;
1620
import org.opensearch.test.framework.TestSecurityConfig;
1721
import org.opensearch.test.framework.cluster.TestRestClient;
1822

1923
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
2024
import static org.hamcrest.CoreMatchers.is;
2125
import static org.hamcrest.MatcherAssert.assertThat;
2226
import static org.hamcrest.Matchers.not;
23-
import static org.opensearch.security.dlic.rest.support.Utils.hash;
2427

2528
public class AccountRestApiIntegrationTest extends AbstractApiIntegrationTest {
2629

@@ -106,11 +109,15 @@ private void verifyWrongPayload(final TestRestClient client) throws Exception {
106109

107110
private void verifyPasswordCanBeChanged() throws Exception {
108111
final var newPassword = randomAlphabetic(10);
112+
final PasswordHasher passwordHasher = new PasswordHasherImpl();
109113
withUser(
110114
TEST_USER,
111115
TEST_USER_PASSWORD,
112116
client -> ok(
113-
() -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, hash(newPassword.toCharArray())))
117+
() -> client.putJson(
118+
accountPath(),
119+
changePasswordWithHashPayload(TEST_USER_PASSWORD, passwordHasher.hash(CharBuffer.wrap(newPassword.toCharArray())))
120+
)
114121
)
115122
);
116123
withUser(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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.awaitility.Awaitility;
18+
import org.junit.BeforeClass;
19+
import org.junit.Test;
20+
21+
import org.opensearch.test.framework.TestSecurityConfig;
22+
import org.opensearch.test.framework.cluster.ClusterManager;
23+
import org.opensearch.test.framework.cluster.LocalCluster;
24+
import org.opensearch.test.framework.cluster.TestRestClient;
25+
26+
import static org.apache.http.HttpStatus.SC_OK;
27+
import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
28+
import static org.hamcrest.Matchers.equalTo;
29+
import static org.opensearch.security.support.ConfigConstants.ARGON2;
30+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM;
31+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS;
32+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH;
33+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY;
34+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM;
35+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE;
36+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION;
37+
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED;
38+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
39+
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
40+
41+
public class Argon2CustomConfigHashingTests extends HashingTests {
42+
43+
public static LocalCluster cluster;
44+
45+
private static String type;
46+
private static int memory, iterations, length, parallelism, version;
47+
48+
@BeforeClass
49+
public static void startCluster() {
50+
51+
type = randomFrom(List.of("D", "I", "ID"));
52+
memory = randomFrom(List.of(4096, 8192, 15360));
53+
iterations = randomFrom((List.of(1, 2, 3, 4)));
54+
length = randomFrom((List.of(4, 8, 16, 32, 64)));
55+
parallelism = randomFrom(List.of(1, 2));
56+
version = randomFrom(List.of(16, 19));
57+
58+
TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
59+
.hash(generateArgon2Hash("secret", type, memory, iterations, length, parallelism, version));
60+
61+
cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
62+
.authc(AUTHC_HTTPBASIC_INTERNAL)
63+
.users(ADMIN_USER)
64+
.anonymousAuth(false)
65+
.nodeSettings(
66+
Map.of(
67+
SECURITY_RESTAPI_ROLES_ENABLED,
68+
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
69+
SECURITY_PASSWORD_HASHING_ALGORITHM,
70+
ARGON2,
71+
SECURITY_PASSWORD_HASHING_ARGON2_TYPE,
72+
type,
73+
SECURITY_PASSWORD_HASHING_ARGON2_MEMORY,
74+
memory,
75+
SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS,
76+
iterations,
77+
SECURITY_PASSWORD_HASHING_ARGON2_LENGTH,
78+
length,
79+
SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM,
80+
parallelism,
81+
SECURITY_PASSWORD_HASHING_ARGON2_VERSION,
82+
version
83+
)
84+
)
85+
.build();
86+
cluster.before();
87+
88+
try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) {
89+
Awaitility.await()
90+
.alias("Load default configuration")
91+
.until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP"));
92+
}
93+
}
94+
95+
@Test
96+
public void shouldAuthenticateWithCorrectPassword() {
97+
String hash = generateArgon2Hash(PASSWORD, type, memory, iterations, length, parallelism, version);
98+
99+
createUserWithHashedPassword(cluster, "user_1", hash);
100+
testPasswordAuth(cluster, "user_1", PASSWORD, SC_OK);
101+
102+
createUserWithPlainTextPassword(cluster, "user_2", PASSWORD);
103+
testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK);
104+
}
105+
106+
@Test
107+
public void shouldNotAuthenticateWithIncorrectPassword() {
108+
String hash = generateArgon2Hash(PASSWORD, type, memory, iterations, length, parallelism, version);
109+
createUserWithHashedPassword(cluster, "user_3", hash);
110+
testPasswordAuth(cluster, "user_3", "wrong_password", SC_UNAUTHORIZED);
111+
112+
createUserWithPlainTextPassword(cluster, "user_4", PASSWORD);
113+
testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED);
114+
}
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.junit.ClassRule;
18+
import org.junit.Test;
19+
20+
import org.opensearch.test.framework.TestSecurityConfig;
21+
import org.opensearch.test.framework.cluster.ClusterManager;
22+
import org.opensearch.test.framework.cluster.LocalCluster;
23+
24+
import static org.apache.http.HttpStatus.SC_OK;
25+
import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
26+
import static org.opensearch.security.support.ConfigConstants.ARGON2;
27+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM;
28+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT;
29+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT;
30+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT;
31+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT;
32+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT;
33+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT;
34+
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED;
35+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
36+
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
37+
38+
public class Argon2DefaultConfigHashingTests extends HashingTests {
39+
private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
40+
.hash(
41+
generateArgon2Hash(
42+
"secret",
43+
SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
44+
SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
45+
SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
46+
SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
47+
SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
48+
SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT
49+
)
50+
);
51+
52+
@ClassRule
53+
public static LocalCluster 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+
SECURITY_RESTAPI_ROLES_ENABLED,
60+
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
61+
SECURITY_PASSWORD_HASHING_ALGORITHM,
62+
ARGON2
63+
)
64+
)
65+
.build();
66+
67+
public void shouldAuthenticateWithCorrectPassword() {
68+
String hash = generateArgon2Hash(
69+
PASSWORD,
70+
SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
71+
SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
72+
SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
73+
SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
74+
SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
75+
SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT
76+
);
77+
createUserWithHashedPassword(cluster, "user_1", hash);
78+
testPasswordAuth(cluster, "user_1", PASSWORD, SC_OK);
79+
80+
createUserWithPlainTextPassword(cluster, "user_2", PASSWORD);
81+
testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK);
82+
}
83+
84+
@Test
85+
public void shouldNotAuthenticateWithIncorrectPassword() {
86+
String hash = generateArgon2Hash(
87+
PASSWORD,
88+
SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
89+
SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
90+
SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
91+
SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
92+
SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
93+
SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT
94+
);
95+
createUserWithHashedPassword(cluster, "user_3", hash);
96+
testPasswordAuth(cluster, "user_3", "wrong_password", SC_UNAUTHORIZED);
97+
98+
createUserWithPlainTextPassword(cluster, "user_4", PASSWORD);
99+
testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED);
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.awaitility.Awaitility;
18+
import org.junit.BeforeClass;
19+
import org.junit.Test;
20+
21+
import org.opensearch.test.framework.TestSecurityConfig;
22+
import org.opensearch.test.framework.cluster.ClusterManager;
23+
import org.opensearch.test.framework.cluster.LocalCluster;
24+
import org.opensearch.test.framework.cluster.TestRestClient;
25+
26+
import static org.apache.http.HttpStatus.SC_OK;
27+
import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
28+
import static org.hamcrest.Matchers.equalTo;
29+
import static org.opensearch.security.support.ConfigConstants.BCRYPT;
30+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM;
31+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR;
32+
import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS;
33+
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED;
34+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
35+
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
36+
37+
public class BCryptCustomConfigHashingTests extends HashingTests {
38+
39+
private static LocalCluster cluster;
40+
41+
private static String minor;
42+
43+
private static int rounds;
44+
45+
@BeforeClass
46+
public static void startCluster() {
47+
minor = randomFrom(List.of("A", "B", "Y"));
48+
rounds = randomIntBetween(4, 10);
49+
50+
TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
51+
.hash(generateBCryptHash("secret", minor, rounds));
52+
cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
53+
.authc(AUTHC_HTTPBASIC_INTERNAL)
54+
.users(ADMIN_USER)
55+
.anonymousAuth(false)
56+
.nodeSettings(
57+
Map.of(
58+
SECURITY_RESTAPI_ROLES_ENABLED,
59+
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
60+
SECURITY_PASSWORD_HASHING_ALGORITHM,
61+
BCRYPT,
62+
SECURITY_PASSWORD_HASHING_BCRYPT_MINOR,
63+
minor,
64+
SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS,
65+
rounds
66+
)
67+
)
68+
.build();
69+
cluster.before();
70+
71+
try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) {
72+
Awaitility.await()
73+
.alias("Load default configuration")
74+
.until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP"));
75+
}
76+
}
77+
78+
@Test
79+
public void shouldAuthenticateWithCorrectPassword() {
80+
String hash = generateBCryptHash(PASSWORD, minor, rounds);
81+
createUserWithHashedPassword(cluster, "user_2", hash);
82+
testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK);
83+
84+
createUserWithPlainTextPassword(cluster, "user_3", PASSWORD);
85+
testPasswordAuth(cluster, "user_3", PASSWORD, SC_OK);
86+
}
87+
88+
@Test
89+
public void shouldNotAuthenticateWithIncorrectPassword() {
90+
String hash = generateBCryptHash(PASSWORD, minor, rounds);
91+
createUserWithHashedPassword(cluster, "user_4", hash);
92+
testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED);
93+
94+
createUserWithPlainTextPassword(cluster, "user_5", PASSWORD);
95+
testPasswordAuth(cluster, "user_5", "wrong_password", SC_UNAUTHORIZED);
96+
}
97+
}

0 commit comments

Comments
 (0)