Skip to content

Commit 8d29b11

Browse files
dancristiancecoiDan Cecoi
andauthored
Add support for PBKDF2 for password hashing & add support for configuring BCrypt and PBKDF2 (#4524)
Signed-off-by: Dan Cecoi <[email protected]> Co-authored-by: Dan Cecoi <[email protected]>
1 parent be92bb6 commit 8d29b11

31 files changed

+1715
-169
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@
3030

3131
import org.opensearch.common.CheckedConsumer;
3232
import org.opensearch.common.CheckedSupplier;
33+
import org.opensearch.common.settings.Settings;
3334
import org.opensearch.common.xcontent.XContentFactory;
3435
import org.opensearch.core.xcontent.ToXContent;
3536
import org.opensearch.core.xcontent.ToXContentObject;
3637
import org.opensearch.security.ConfigurationFiles;
3738
import org.opensearch.security.dlic.rest.api.Endpoint;
38-
import org.opensearch.security.hasher.BCryptPasswordHasher;
3939
import org.opensearch.security.hasher.PasswordHasher;
40+
import org.opensearch.security.hasher.PasswordHasherFactory;
4041
import org.opensearch.security.securityconf.impl.CType;
42+
import org.opensearch.security.support.ConfigConstants;
4143
import org.opensearch.test.framework.TestSecurityConfig;
4244
import org.opensearch.test.framework.certificate.CertificateData;
4345
import org.opensearch.test.framework.cluster.ClusterManager;
@@ -78,6 +80,10 @@ public abstract class AbstractApiIntegrationTest extends RandomizedTest {
7880

7981
public static final ToXContentObject EMPTY_BODY = (builder, params) -> builder.startObject().endObject();
8082

83+
public static final PasswordHasher passwordHasher = PasswordHasherFactory.createPasswordHasher(
84+
Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.BCRYPT).build()
85+
);
86+
8187
public static Path configurationFolder;
8288

8389
public static ImmutableMap.Builder<String, Object> clusterSettings = ImmutableMap.builder();
@@ -86,8 +92,6 @@ public abstract class AbstractApiIntegrationTest extends RandomizedTest {
8692

8793
public static LocalCluster localCluster;
8894

89-
public static PasswordHasher passwordHasher = new BCryptPasswordHasher();
90-
9195
@BeforeClass
9296
public static void startCluster() throws IOException {
9397
configurationFolder = ConfigurationFiles.createConfigurationDirectory();

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232
import org.opensearch.core.xcontent.ToXContentObject;
3333
import org.opensearch.security.DefaultObjectMapper;
3434
import org.opensearch.security.dlic.rest.api.Endpoint;
35-
import org.opensearch.security.hasher.BCryptPasswordHasher;
36-
import org.opensearch.security.hasher.PasswordHasher;
3735
import org.opensearch.test.framework.TestSecurityConfig;
3836
import org.opensearch.test.framework.cluster.TestRestClient;
3937
import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse;
@@ -59,8 +57,6 @@ public class InternalUsersRestApiIntegrationTest extends AbstractConfigEntityApi
5957

6058
private final static String SOME_ROLE = "some-role";
6159

62-
private final PasswordHasher passwordHasher = new BCryptPasswordHasher();
63-
6460
static {
6561
testSecurityConfig.withRestAdminUser(REST_API_ADMIN_INTERNAL_USERS_ONLY, restAdminPermission(Endpoint.INTERNALUSERS))
6662
.user(new TestSecurityConfig.User(SERVICE_ACCOUNT_USER).attr("service", "true").attr("enabled", "true"))
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 BCryptCustomConfigHashingTests extends HashingTests {
33+
34+
private static LocalCluster cluster;
35+
36+
private static String minor;
37+
38+
private static int rounds;
39+
40+
@BeforeClass
41+
public static void startCluster() {
42+
minor = randomFrom(List.of("A", "B", "Y"));
43+
rounds = randomIntBetween(4, 10);
44+
45+
TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
46+
.hash(generateBCryptHash("secret", minor, rounds));
47+
cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
48+
.authc(AUTHC_HTTPBASIC_INTERNAL)
49+
.users(ADMIN_USER)
50+
.anonymousAuth(false)
51+
.nodeSettings(
52+
Map.of(
53+
ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED,
54+
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
55+
ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM,
56+
ConfigConstants.BCRYPT,
57+
ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR,
58+
minor,
59+
ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS,
60+
rounds
61+
)
62+
)
63+
.build();
64+
cluster.before();
65+
66+
try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) {
67+
Awaitility.await()
68+
.alias("Load default configuration")
69+
.until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP"));
70+
}
71+
}
72+
73+
@Test
74+
public void shouldAuthenticateWithCorrectPassword() {
75+
String hash = generateBCryptHash(PASSWORD, minor, rounds);
76+
createUserWithHashedPassword(cluster, "user_2", hash);
77+
testPasswordAuth(cluster, "user_2", PASSWORD, HttpStatus.SC_OK);
78+
79+
createUserWithPlainTextPassword(cluster, "user_3", PASSWORD);
80+
testPasswordAuth(cluster, "user_3", PASSWORD, HttpStatus.SC_OK);
81+
}
82+
83+
@Test
84+
public void shouldNotAuthenticateWithIncorrectPassword() {
85+
String hash = generateBCryptHash(PASSWORD, minor, rounds);
86+
createUserWithHashedPassword(cluster, "user_4", hash);
87+
testPasswordAuth(cluster, "user_4", "wrong_password", HttpStatus.SC_UNAUTHORIZED);
88+
89+
createUserWithPlainTextPassword(cluster, "user_5", PASSWORD);
90+
testPasswordAuth(cluster, "user_5", "wrong_password", HttpStatus.SC_UNAUTHORIZED);
91+
}
92+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 BCryptDefaultConfigHashingTests extends HashingTests {
30+
31+
private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS);
32+
33+
@ClassRule
34+
public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
35+
.authc(AUTHC_HTTPBASIC_INTERNAL)
36+
.users(ADMIN_USER)
37+
.anonymousAuth(false)
38+
.nodeSettings(
39+
Map.of(ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()))
40+
)
41+
.build();
42+
43+
@Test
44+
public void shouldAuthenticateWithCorrectPassword() {
45+
String hash = generateBCryptHash(
46+
PASSWORD,
47+
ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT,
48+
ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT
49+
);
50+
createUserWithHashedPassword(cluster, "user_2", hash);
51+
testPasswordAuth(cluster, "user_2", PASSWORD, HttpStatus.SC_OK);
52+
53+
createUserWithPlainTextPassword(cluster, "user_3", PASSWORD);
54+
testPasswordAuth(cluster, "user_3", PASSWORD, HttpStatus.SC_OK);
55+
}
56+
57+
@Test
58+
public void shouldNotAuthenticateWithIncorrectPassword() {
59+
String hash = generateBCryptHash(
60+
PASSWORD,
61+
ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT,
62+
ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT
63+
);
64+
createUserWithHashedPassword(cluster, "user_4", hash);
65+
testPasswordAuth(cluster, "user_4", "wrong_password", HttpStatus.SC_UNAUTHORIZED);
66+
67+
createUserWithPlainTextPassword(cluster, "user_5", PASSWORD);
68+
testPasswordAuth(cluster, "user_5", "wrong_password", HttpStatus.SC_UNAUTHORIZED);
69+
}
70+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.nio.CharBuffer;
15+
16+
import com.carrotsearch.randomizedtesting.RandomizedTest;
17+
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
18+
import org.apache.http.HttpStatus;
19+
import org.junit.runner.RunWith;
20+
21+
import org.opensearch.test.framework.TestSecurityConfig;
22+
import org.opensearch.test.framework.cluster.LocalCluster;
23+
import org.opensearch.test.framework.cluster.TestRestClient;
24+
25+
import com.password4j.BcryptFunction;
26+
import com.password4j.CompressedPBKDF2Function;
27+
import com.password4j.Password;
28+
import com.password4j.types.Bcrypt;
29+
30+
import static org.hamcrest.MatcherAssert.assertThat;
31+
import static org.hamcrest.Matchers.equalTo;
32+
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
33+
34+
@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
35+
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
36+
public class HashingTests extends RandomizedTest {
37+
38+
private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS);
39+
40+
static final String PASSWORD = "top$ecret1234!";
41+
42+
public void createUserWithPlainTextPassword(LocalCluster cluster, String username, String password) {
43+
try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) {
44+
TestRestClient.HttpResponse httpResponse = client.putJson(
45+
"_plugins/_security/api/internalusers/" + username,
46+
String.format("{\"password\": \"%s\",\"opendistro_security_roles\": []}", password)
47+
);
48+
assertThat(httpResponse.getStatusCode(), equalTo(HttpStatus.SC_CREATED));
49+
}
50+
}
51+
52+
public void createUserWithHashedPassword(LocalCluster cluster, String username, String hashedPassword) {
53+
try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) {
54+
TestRestClient.HttpResponse httpResponse = client.putJson(
55+
"_plugins/_security/api/internalusers/" + username,
56+
String.format("{\"hash\": \"%s\",\"opendistro_security_roles\": []}", hashedPassword)
57+
);
58+
assertThat(httpResponse.getStatusCode(), equalTo(HttpStatus.SC_CREATED));
59+
}
60+
}
61+
62+
public void testPasswordAuth(LocalCluster cluster, String username, String password, int expectedStatusCode) {
63+
try (TestRestClient client = cluster.getRestClient(username, password)) {
64+
TestRestClient.HttpResponse response = client.getAuthInfo();
65+
response.assertStatusCode(expectedStatusCode);
66+
}
67+
}
68+
69+
public static String generateBCryptHash(String password, String minor, int rounds) {
70+
return Password.hash(CharBuffer.wrap(password.toCharArray()))
71+
.with(BcryptFunction.getInstance(Bcrypt.valueOf(minor), rounds))
72+
.getResult();
73+
}
74+
75+
public static String generatePBKDF2Hash(String password, String algorithm, int iterations, int length) {
76+
return Password.hash(CharBuffer.wrap(password.toCharArray()))
77+
.with(CompressedPBKDF2Function.getInstance(algorithm, iterations, length))
78+
.getResult();
79+
}
80+
81+
}
Lines changed: 97 additions & 0 deletions
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.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 PBKDF2CustomConfigHashingTests extends HashingTests {
33+
34+
public static LocalCluster cluster;
35+
36+
private static final String PASSWORD = "top$ecret1234!";
37+
38+
private static String function;
39+
private static int iterations, length;
40+
41+
@BeforeClass
42+
public static void startCluster() {
43+
44+
function = randomFrom(List.of("SHA224", "SHA256", "SHA384", "SHA512"));
45+
iterations = randomFrom(List.of(32000, 64000, 128000, 256000));
46+
length = randomFrom(List.of(128, 256, 512));
47+
48+
TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
49+
.hash(generatePBKDF2Hash("secret", function, iterations, length));
50+
cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
51+
.authc(AUTHC_HTTPBASIC_INTERNAL)
52+
.users(ADMIN_USER)
53+
.anonymousAuth(false)
54+
.nodeSettings(
55+
Map.of(
56+
ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED,
57+
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
58+
ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM,
59+
ConfigConstants.PBKDF2,
60+
ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION,
61+
function,
62+
ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS,
63+
iterations,
64+
ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH,
65+
length
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 = generatePBKDF2Hash(PASSWORD, function, iterations, length);
81+
createUserWithHashedPassword(cluster, "user_1", hash);
82+
testPasswordAuth(cluster, "user_1", PASSWORD, HttpStatus.SC_OK);
83+
84+
createUserWithPlainTextPassword(cluster, "user_2", PASSWORD);
85+
testPasswordAuth(cluster, "user_2", PASSWORD, HttpStatus.SC_OK);
86+
}
87+
88+
@Test
89+
public void shouldNotAuthenticateWithIncorrectPassword() {
90+
String hash = generatePBKDF2Hash(PASSWORD, function, iterations, length);
91+
createUserWithHashedPassword(cluster, "user_3", hash);
92+
testPasswordAuth(cluster, "user_3", "wrong_password", HttpStatus.SC_UNAUTHORIZED);
93+
94+
createUserWithPlainTextPassword(cluster, "user_4", PASSWORD);
95+
testPasswordAuth(cluster, "user_4", "wrong_password", HttpStatus.SC_UNAUTHORIZED);
96+
}
97+
}

0 commit comments

Comments
 (0)