From a51584d22078d2c9614ce6f9982335debaf58a51 Mon Sep 17 00:00:00 2001 From: Dan Cecoi Date: Wed, 17 Apr 2024 14:03:47 +0100 Subject: [PATCH] Add support for PBKDF2, Scrypt, and Argon2 for password hashing (#4079) Signed-off-by: Dan Cecoi --- build.gradle | 2 + plugin-security.policy | 3 + .../api/AccountRestApiIntegrationTest.java | 9 +- .../hash/Argon2CustomConfigHashingTests.java | 112 +++++++ .../hash/Argon2DefaultConfigHashingTests.java | 76 +++++ .../hash/BCryptCustomConfigHashingTests.java | 78 +++++ .../hash/BCryptDefaultConfigHashingTests.java | 78 +++++ .../security/hash/HashingTests.java | 106 +++++++ .../hash/PBKDF2CustomConfigHashingTests.java | 96 ++++++ .../hash/PBKDF2DefaultConfigHashingTests.java | 70 +++++ .../hash/SCryptCustomConfigHashingTests.java | 95 ++++++ .../hash/SCryptDefaultConfigHashingTests.java | 71 +++++ .../test/framework/TestSecurityConfig.java | 42 ++- .../security/OpenSearchSecurityPlugin.java | 115 +++++++ .../InternalAuthenticationBackend.java | 12 +- .../dlic/rest/api/AccountApiAction.java | 14 +- .../dlic/rest/api/InternalUsersApiAction.java | 9 +- .../security/dlic/rest/support/Utils.java | 20 +- .../security/hash/PasswordHasher.java | 19 ++ .../security/hash/PasswordHasherImpl.java | 146 +++++++++ .../securityconf/DynamicConfigFactory.java | 3 +- .../security/support/ConfigConstants.java | 38 +++ .../org/opensearch/security/tools/Hasher.java | 288 +++++++++++++++--- .../opensearch/security/user/UserService.java | 14 +- .../security/PasswordHasherTest.java | 116 +++++++ .../org/opensearch/security/UtilTests.java | 4 +- .../auth/InternalAuthBackendTests.java | 3 +- .../dlic/dlsfls/DfmOverwritesAllTest.java | 3 +- .../api/AbstractApiActionValidationTest.java | 6 + ...AccountApiActionConfigValidationsTest.java | 9 +- .../InternalUsersApiActionValidationTest.java | 4 +- .../security/tools/HasherTests.java | 172 +++++++++++ 32 files changed, 1741 insertions(+), 92 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/hash/Argon2CustomConfigHashingTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/hash/Argon2DefaultConfigHashingTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/hash/BCryptCustomConfigHashingTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/hash/BCryptDefaultConfigHashingTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/hash/HashingTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/hash/PBKDF2CustomConfigHashingTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/hash/PBKDF2DefaultConfigHashingTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/hash/SCryptCustomConfigHashingTests.java create mode 100644 src/integrationTest/java/org/opensearch/security/hash/SCryptDefaultConfigHashingTests.java create mode 100644 src/main/java/org/opensearch/security/hash/PasswordHasher.java create mode 100644 src/main/java/org/opensearch/security/hash/PasswordHasherImpl.java create mode 100644 src/test/java/org/opensearch/security/PasswordHasherTest.java create mode 100644 src/test/java/org/opensearch/security/tools/HasherTests.java diff --git a/build.gradle b/build.gradle index c4cf77a5e8..85b58ad693 100644 --- a/build.gradle +++ b/build.gradle @@ -583,6 +583,8 @@ dependencies { implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3' implementation 'com.rfksystems:blake2b:2.0.0' + implementation 'com.password4j:password4j:1.7.3' + //JWT implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}" implementation "io.jsonwebtoken:jjwt-impl:${jjwt_version}" diff --git a/plugin-security.policy b/plugin-security.policy index 6a78a5cc91..7aaa664939 100644 --- a/plugin-security.policy +++ b/plugin-security.policy @@ -78,6 +78,9 @@ grant { //Enable this permission to debug unauthorized de-serialization attempt //permission java.io.SerializablePermission "enableSubstitution"; + + permission java.io.FilePermission "/psw4j.properties","read"; + }; grant codeBase "${codebase.netty-common}" { diff --git a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java index 7fa298c1e4..12a6201c38 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java @@ -13,14 +13,17 @@ import org.junit.Test; import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.security.hash.PasswordHasher; +import org.opensearch.security.hash.PasswordHasherImpl; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.TestRestClient; +import java.nio.CharBuffer; + import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.not; -import static org.opensearch.security.dlic.rest.support.Utils.hash; public class AccountRestApiIntegrationTest extends AbstractApiIntegrationTest { @@ -106,11 +109,13 @@ private void verifyWrongPayload(final TestRestClient client) throws Exception { private void verifyPasswordCanBeChanged() throws Exception { final var newPassword = randomAlphabetic(10); + final PasswordHasher passwordHasher = new PasswordHasherImpl(); withUser( TEST_USER, TEST_USER_PASSWORD, client -> ok( - () -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, hash(newPassword.toCharArray()))) + () -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, + passwordHasher.hash(CharBuffer.wrap(newPassword.toCharArray())))) ) ); withUser( diff --git a/src/integrationTest/java/org/opensearch/security/hash/Argon2CustomConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/Argon2CustomConfigHashingTests.java new file mode 100644 index 0000000000..29689be677 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/hash/Argon2CustomConfigHashingTests.java @@ -0,0 +1,112 @@ +package org.opensearch.security.hash; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.awaitility.Awaitility; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import java.util.List; +import java.util.Map; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.support.ConfigConstants.*; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + + +public class Argon2CustomConfigHashingTests extends HashingTests { + + public static LocalCluster cluster; + + private static String type; + private static int memory, iterations, length, parallelism, version; + + @BeforeClass + public static void startCluster(){ + + type = randomFrom(List.of("D", "I", "ID")); + memory = randomFrom(List.of(4096, 8192, 15360)); + iterations = randomFrom((List.of(1,2,3,4))); + length = randomFrom((List.of(4,8,16,32,64))); + parallelism = randomFrom(List.of(1,2)); + version = randomFrom(List.of(16,19)); + + TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin") + .roles(ALL_ACCESS) + .hash(generateArgon2Hash( + "secret", + type, + memory, + iterations, + length, + parallelism, + version)); + + cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .anonymousAuth(false) + .nodeSettings(Map.of( + SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_PASSWORD_HASHING_ALGORITHM, ARGON2, + SECURITY_PASSWORD_HASHING_ARGON2_TYPE, type, + SECURITY_PASSWORD_HASHING_ARGON2_MEMORY, memory, + SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS, iterations, + SECURITY_PASSWORD_HASHING_ARGON2_LENGTH, length, + SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM, parallelism, + SECURITY_PASSWORD_HASHING_ARGON2_VERSION, version + )) + .build(); + cluster.before(); + + try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) { + Awaitility.await() + .alias("Load default configuration") + .until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP")); + } + } + + @Test + public void shouldAuthenticateWithCorrectPassword(){ + String hash = generateArgon2Hash( + PASSWORD, + type, + memory, + iterations, + length, + parallelism, + version + ); + + createUserWithHashedPassword(cluster, "user_1", hash); + testPasswordAuth(cluster, "user_1", PASSWORD, SC_OK); + + createUserWithPlainTextPassword(cluster, "user_2", PASSWORD); + testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK); + } + + @Test + public void shouldNotAuthenticateWithIncorrectPassword(){ + String hash = generateArgon2Hash( + PASSWORD, + type, + memory, + iterations, + length, + parallelism, + version + ); + createUserWithHashedPassword(cluster, "user_3", hash); + testPasswordAuth(cluster, "user_3", "wrong_password", SC_UNAUTHORIZED); + + createUserWithPlainTextPassword(cluster, "user_4", PASSWORD); + testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/hash/Argon2DefaultConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/Argon2DefaultConfigHashingTests.java new file mode 100644 index 0000000000..15c32eac5e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/hash/Argon2DefaultConfigHashingTests.java @@ -0,0 +1,76 @@ +package org.opensearch.security.hash; + +import org.junit.ClassRule; +import org.junit.Test; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import java.util.List; +import java.util.Map; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.opensearch.security.support.ConfigConstants.*; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +public class Argon2DefaultConfigHashingTests extends HashingTests { + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin") + .roles(ALL_ACCESS) + .hash(generateArgon2Hash( + "secret", + SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT + )); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .anonymousAuth(false) + .nodeSettings(Map.of( + SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_PASSWORD_HASHING_ALGORITHM, ARGON2 + )) + .build(); + + public void shouldAuthenticateWithCorrectPassword(){ + String hash = generateArgon2Hash( + PASSWORD, + SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT + ); + createUserWithHashedPassword(cluster, "user_1", hash); + testPasswordAuth(cluster, "user_1", PASSWORD, SC_OK); + + createUserWithPlainTextPassword(cluster, "user_2", PASSWORD); + testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK); + } + + @Test + public void shouldNotAuthenticateWithIncorrectPassword(){ + String hash = generateArgon2Hash( + PASSWORD, + SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT, + SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT + ); + createUserWithHashedPassword(cluster, "user_3", hash); + testPasswordAuth(cluster, "user_3", "wrong_password", SC_UNAUTHORIZED); + + createUserWithPlainTextPassword(cluster, "user_4", PASSWORD); + testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/hash/BCryptCustomConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/BCryptCustomConfigHashingTests.java new file mode 100644 index 0000000000..1b7e800b17 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/hash/BCryptCustomConfigHashingTests.java @@ -0,0 +1,78 @@ +package org.opensearch.security.hash; + +import org.awaitility.Awaitility; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + + +import java.util.List; +import java.util.Map; + +import static org.apache.http.HttpStatus.*; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.support.ConfigConstants.*; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + + +public class BCryptCustomConfigHashingTests extends HashingTests { + + private static LocalCluster cluster; + + private static String minor; + + private static int rounds; + + @BeforeClass + public static void startCluster(){ + minor = randomFrom(List.of("A", "B", "Y")); + rounds = randomIntBetween(4, 10); + + TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin") + .roles(ALL_ACCESS) + .hash(generateBCryptHash("secret", minor, rounds)); + cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .anonymousAuth(false) + .nodeSettings(Map.of( + SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_PASSWORD_HASHING_ALGORITHM, BCRYPT, + SECURITY_PASSWORD_HASHING_BCRYPT_MINOR, minor, + SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS, rounds + )) + .build(); + cluster.before(); + + try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) { + Awaitility.await() + .alias("Load default configuration") + .until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP")); + } + } + + @Test + public void shouldAuthenticateWithCorrectPassword(){ + String hash = generateBCryptHash(PASSWORD, minor, rounds); + createUserWithHashedPassword(cluster, "user_2", hash); + testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK); + + createUserWithPlainTextPassword(cluster, "user_3", PASSWORD); + testPasswordAuth(cluster, "user_3", PASSWORD, SC_OK); + } + + @Test + public void shouldNotAuthenticateWithIncorrectPassword(){ + String hash = generateBCryptHash(PASSWORD, minor, rounds); + createUserWithHashedPassword(cluster, "user_4", hash); + testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED); + + createUserWithPlainTextPassword(cluster, "user_5", PASSWORD); + testPasswordAuth(cluster, "user_5", "wrong_password", SC_UNAUTHORIZED); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/hash/BCryptDefaultConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/BCryptDefaultConfigHashingTests.java new file mode 100644 index 0000000000..e4ecaafbb8 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/hash/BCryptDefaultConfigHashingTests.java @@ -0,0 +1,78 @@ +package org.opensearch.security.hash; + +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.junit.ClassRule; +import org.junit.Test; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.apache.http.HttpStatus.*; + +import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT; +import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT; + +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + + +public class BCryptDefaultConfigHashingTests extends HashingTests { + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .anonymousAuth(false) + .nodeSettings(Map.of(SECURITY_RESTAPI_ROLES_ENABLED, + List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()))) + .build(); + + @Test + public void shouldAuthenticateWhenUserCreatedWithLegacyHash(){ + String hash = generateLegacyBCryptHash(PASSWORD.toCharArray()); + createUserWithHashedPassword(cluster, "user_1", hash); + testPasswordAuth(cluster, "user_1", PASSWORD, SC_OK); + } + + @Test + public void shouldAuthenticateWithCorrectPassword(){ + String hash = generateBCryptHash(PASSWORD, + SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT, + SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT); + createUserWithHashedPassword(cluster, "user_2", hash); + testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK); + + createUserWithPlainTextPassword(cluster, "user_3", PASSWORD); + testPasswordAuth(cluster, "user_3", PASSWORD, SC_OK); + } + + @Test + public void shouldNotAuthenticateWithIncorrectPassword(){ + String hash = generateBCryptHash(PASSWORD, + SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT, + SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT); + createUserWithHashedPassword(cluster, "user_4", hash); + testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED); + + createUserWithPlainTextPassword(cluster, "user_5", PASSWORD); + testPasswordAuth(cluster, "user_5", "wrong_password", SC_UNAUTHORIZED); + } + + private String generateLegacyBCryptHash(final char[] clearTextPassword){ + final byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); + Arrays.fill(salt, (byte) 0); + Arrays.fill(clearTextPassword, '\0'); + return hash; + } +} diff --git a/src/integrationTest/java/org/opensearch/security/hash/HashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/HashingTests.java new file mode 100644 index 0000000000..99d13b4811 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/hash/HashingTests.java @@ -0,0 +1,106 @@ +package org.opensearch.security.hash; + + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import com.password4j.*; +import com.password4j.types.Argon2; +import com.password4j.types.Bcrypt; +import org.junit.runner.RunWith; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import java.nio.CharBuffer; + +import static org.apache.http.HttpStatus.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class HashingTests extends RandomizedTest { + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + static final String PASSWORD = "top$ecret1234!"; + + public void createUserWithPlainTextPassword(LocalCluster cluster, + String username, + String password){ + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + TestRestClient.HttpResponse httpResponse = client.putJson( + "_plugins/_security/api/internalusers/" + username, + String.format("{\"password\": \"%s\",\"opendistro_security_roles\": []}", password) + ); + assertThat(httpResponse.getStatusCode(), equalTo(SC_CREATED)); + } + } + + public void createUserWithHashedPassword(LocalCluster cluster, + String username, + String hashedPassword){ + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + TestRestClient.HttpResponse httpResponse = client.putJson( + "_plugins/_security/api/internalusers/" + username, + String.format("{\"hash\": \"%s\",\"opendistro_security_roles\": []}", hashedPassword) + ); + assertThat(httpResponse.getStatusCode(), equalTo(SC_CREATED)); + } + } + + public void testPasswordAuth(LocalCluster cluster, + String username, + String password, + int expectedStatusCode){ + try (TestRestClient client = cluster.getRestClient(username, password)){ + TestRestClient.HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(expectedStatusCode); + } + } + + public static String generateBCryptHash(String password, + String minor, + int rounds){ + return Password + .hash(CharBuffer.wrap(password.toCharArray())) + .with(BcryptFunction.getInstance(Bcrypt.valueOf(minor), rounds)) + .getResult(); + } + + public static String generateSCryptHash(String password, + int workFactor, + int resources, + int parallelization, + int derivedKeyLength){ + return Password + .hash(CharBuffer.wrap(password.toCharArray())) + .with(ScryptFunction.getInstance(workFactor, resources, parallelization, derivedKeyLength)) + .getResult(); + } + + public static String generateArgon2Hash(String password, + String type, + int memory, + int iterations, + int length, + int parallelism, + int version){ + return Password + .hash(CharBuffer.wrap(password.toCharArray())) + .with(Argon2Function.getInstance( + memory, iterations, parallelism, length, Argon2.valueOf(type.toUpperCase()), version)) + .getResult(); + } + + public static String generatePBKDF2Hash(String password, String algorithm, int iterations, int length){ + return Password + .hash(CharBuffer.wrap(password.toCharArray())) + .with(CompressedPBKDF2Function.getInstance( + algorithm, iterations, length + )) + .getResult(); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/hash/PBKDF2CustomConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/PBKDF2CustomConfigHashingTests.java new file mode 100644 index 0000000000..a1628a30e9 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/hash/PBKDF2CustomConfigHashingTests.java @@ -0,0 +1,96 @@ +package org.opensearch.security.hash; + +import org.awaitility.Awaitility; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import java.util.List; +import java.util.Map; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.support.ConfigConstants.*; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + + +public class PBKDF2CustomConfigHashingTests extends HashingTests { + + public static LocalCluster cluster; + + private static final String PASSWORD = "top$ecret1234!"; + + private static String function; + private static int iterations, length; + + @BeforeClass + public static void startCluster(){ + + function = randomFrom(List.of("SHA224", "SHA256", "SHA384", "SHA512")); + iterations = randomFrom(List.of(32000, 64000, 128000, 256000)); + length = randomFrom(List.of(128, 256,512)); + + TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin") + .roles(ALL_ACCESS) + .hash(generatePBKDF2Hash( + "secret", + function, + iterations, + length + )); + cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .anonymousAuth(false) + .nodeSettings(Map.of( + SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_PASSWORD_HASHING_ALGORITHM, PBKDF2, + SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION, function, + SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS, iterations, + SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH, length + )) + .build(); + cluster.before(); + + try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) { + Awaitility.await() + .alias("Load default configuration") + .until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP")); + } + } + + @Test + public void shouldAuthenticateWithCorrectPassword(){ + String hash = generatePBKDF2Hash( + PASSWORD, + function, + iterations, + length + ); + createUserWithHashedPassword(cluster, "user_1", hash); + testPasswordAuth(cluster, "user_1", PASSWORD, SC_OK); + + createUserWithPlainTextPassword(cluster, "user_2", PASSWORD); + testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK); + } + + @Test + public void shouldNotAuthenticateWithIncorrectPassword(){ + String hash = generatePBKDF2Hash( + PASSWORD, + function, + iterations, + length + ); + createUserWithHashedPassword(cluster, "user_3", hash); + testPasswordAuth(cluster, "user_3", "wrong_password", SC_UNAUTHORIZED); + + createUserWithPlainTextPassword(cluster, "user_4", PASSWORD); + testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/hash/PBKDF2DefaultConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/PBKDF2DefaultConfigHashingTests.java new file mode 100644 index 0000000000..1af5344729 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/hash/PBKDF2DefaultConfigHashingTests.java @@ -0,0 +1,70 @@ +package org.opensearch.security.hash; + +import org.junit.ClassRule; +import org.junit.Test; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import java.util.List; +import java.util.Map; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.opensearch.security.support.ConfigConstants.*; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + + +public class PBKDF2DefaultConfigHashingTests extends HashingTests { + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin") + .roles(ALL_ACCESS) + .hash(generatePBKDF2Hash( + "secret", + SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT, + SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT, + SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT + )); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .anonymousAuth(false) + .nodeSettings(Map.of( + SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_PASSWORD_HASHING_ALGORITHM, PBKDF2 + )) + .build(); + + @Test + public void shouldAuthenticateWithCorrectPassword(){ + String hash = generatePBKDF2Hash( + PASSWORD, + SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT, + SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT, + SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT + ); + createUserWithHashedPassword(cluster, "user_1", hash); + testPasswordAuth(cluster, "user_1", PASSWORD, SC_OK); + + createUserWithPlainTextPassword(cluster, "user_2", PASSWORD); + testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK); + } + + @Test + public void shouldNotAuthenticateWithIncorrectPassword(){ + String hash = generatePBKDF2Hash( + PASSWORD, + SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT, + SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT, + SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT + ); + createUserWithHashedPassword(cluster, "user_3", hash); + testPasswordAuth(cluster, "user_3", "wrong_password", SC_UNAUTHORIZED); + + createUserWithPlainTextPassword(cluster, "user_4", PASSWORD); + testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/hash/SCryptCustomConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/SCryptCustomConfigHashingTests.java new file mode 100644 index 0000000000..16d0eebe36 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/hash/SCryptCustomConfigHashingTests.java @@ -0,0 +1,95 @@ +package org.opensearch.security.hash; + +import org.awaitility.Awaitility; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import java.util.List; +import java.util.Map; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.support.ConfigConstants.*; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + + +public class SCryptCustomConfigHashingTests extends HashingTests { + + public static LocalCluster cluster; + + private static int workFactor, resources, parallelization, derivedKeyLength; + + @BeforeClass + public static void startCluster(){ + + workFactor = randomFrom(List.of(2048, 16384)); + resources = randomFrom(List.of(4,8)); + parallelization = randomFrom(List.of(1,2)); + derivedKeyLength = randomFrom(List.of(32,64)); + + TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin") + .roles(ALL_ACCESS) + .hash(generateSCryptHash( + "secret", + workFactor, + resources, + parallelization, + derivedKeyLength)); + cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .anonymousAuth(false) + .nodeSettings(Map.of( + SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_PASSWORD_HASHING_ALGORITHM, SCRYPT, + SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR, workFactor, + SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES, resources, + SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION, parallelization, + SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH, derivedKeyLength + )) + .build(); + cluster.before(); + + try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) { + Awaitility.await() + .alias("Load default configuration") + .until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP")); + } + } + + @Test + public void shouldAuthenticateWithCorrectPassword(){ + String hash = generateSCryptHash( + PASSWORD, + workFactor, + resources, + parallelization, + derivedKeyLength); + createUserWithHashedPassword(cluster, "user_1", hash); + testPasswordAuth(cluster, "user_1", PASSWORD, SC_OK); + + createUserWithPlainTextPassword(cluster, "user_2", PASSWORD); + testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK); + } + + @Test + public void shouldNotAuthenticateWithIncorrectPassword(){ + String hash = generateSCryptHash( + PASSWORD, + workFactor, + resources, + parallelization, + derivedKeyLength); + createUserWithHashedPassword(cluster, "user_3", hash); + testPasswordAuth(cluster, "user_3", "wrong_password", SC_UNAUTHORIZED); + + createUserWithPlainTextPassword(cluster, "user_4", PASSWORD); + testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/hash/SCryptDefaultConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/SCryptDefaultConfigHashingTests.java new file mode 100644 index 0000000000..1eff347a25 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/hash/SCryptDefaultConfigHashingTests.java @@ -0,0 +1,71 @@ +package org.opensearch.security.hash; + +import org.junit.ClassRule; +import org.junit.Test; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import java.util.List; +import java.util.Map; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.opensearch.security.support.ConfigConstants.*; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + + +public class SCryptDefaultConfigHashingTests extends HashingTests { + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin") + .roles(ALL_ACCESS) + .hash(generateSCryptHash( + "secret", + SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR_DEFAULT, + SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES_DEFAULT, + SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION_DEFAULT, + SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH_DEFAULT + )); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .anonymousAuth(false) + .nodeSettings(Map.of( + SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_PASSWORD_HASHING_ALGORITHM, SCRYPT + )) + .build(); + + @Test + public void shouldAuthenticateWithCorrectPassword(){ + String hash = generateSCryptHash( + PASSWORD, + SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR_DEFAULT, + SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES_DEFAULT, + SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION_DEFAULT, + SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH_DEFAULT); + createUserWithHashedPassword(cluster, "user_1", hash); + testPasswordAuth(cluster, "user_1", PASSWORD, SC_OK); + + createUserWithPlainTextPassword(cluster, "user_2", PASSWORD); + testPasswordAuth(cluster, "user_2", PASSWORD, SC_OK); + } + + @Test + public void shouldNotAuthenticateWithIncorrectPassword(){ + String hash = generateSCryptHash( + PASSWORD, + SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR_DEFAULT, + SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES_DEFAULT, + SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION_DEFAULT, + SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH_DEFAULT); + createUserWithHashedPassword(cluster, "user_3", hash); + testPasswordAuth(cluster, "user_3", "wrong_password", SC_UNAUTHORIZED); + + createUserWithPlainTextPassword(cluster, "user_4", PASSWORD); + testPasswordAuth(cluster, "user_4", "wrong_password", SC_UNAUTHORIZED); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 79f10a76cf..e433056467 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -31,7 +31,8 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; -import java.security.SecureRandom; +import java.nio.CharBuffer; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -46,22 +47,25 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.update.UpdateRequest; import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.core.common.Strings; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.hash.PasswordHasher; +import org.opensearch.security.hash.PasswordHasherImpl; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.security.support.ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM; /** * This class allows the declarative specification of the security configuration; in particular: @@ -90,9 +94,9 @@ public class TestSecurityConfig { private String indexName = ".opendistro_security"; - public TestSecurityConfig() { + public TestSecurityConfig() {} - } + public TestSecurityConfig(Settings settings){} public TestSecurityConfig configIndexName(String configIndexName) { this.indexName = configIndexName; @@ -387,6 +391,17 @@ public static final class User implements UserCredentialsHolder, ToXContentObjec private String description; + private String hash; + + public String getHash() { + return hash; + } + + public User hash(String hash) { + this.hash = hash; + return this; + } + public User(String name) { this(name, null); } @@ -451,7 +466,11 @@ public Object getAttribute(String attributeName) { public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { xContentBuilder.startObject(); - xContentBuilder.field("hash", hash(password.toCharArray())); + if (this.hash == null){ + xContentBuilder.field("hash", hashPassword(password)); + }else{ + xContentBuilder.field("hash", hash); + } Set roleNames = getRoleNames(); @@ -933,13 +952,12 @@ public void updateInternalUsersConfiguration(Client client, List users) { updateConfigInIndex(client, CType.INTERNALUSERS, userMap); } - static String hash(final char[] clearTextPassword) { - final byte[] salt = new byte[16]; - new SecureRandom().nextBytes(salt); - final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); - Arrays.fill(salt, (byte) 0); - Arrays.fill(clearTextPassword, '\0'); - return hash; + static String hashPassword(final String clearTextPassword) { + PasswordHasher passwordHasher = new PasswordHasherImpl(); + return passwordHasher.hash(( + Objects.requireNonNull( + CharBuffer.wrap(clearTextPassword.toCharArray()) + ))); } private void writeEmptyConfigToIndex(Client client, CType configType) { diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 6e3c22e695..dba8b0e4ef 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1916,6 +1916,121 @@ public List> getSettings() { ); } + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT, + Property.NodeScope, + Property.Final + ) + ); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.simpleString( + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.simpleString( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT, + Property.NodeScope, + Property.Final + )); + + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS, + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT, + Property.NodeScope, + Property.Final + )); + + settings.add(Setting.simpleString( + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR, + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT, + Property.NodeScope, + Property.Final + )); + return settings; } diff --git a/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java b/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java index d3dba7409e..0a05902948 100644 --- a/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java +++ b/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java @@ -35,11 +35,12 @@ import java.util.Map; import java.util.Map.Entry; -import org.bouncycastle.crypto.generators.OpenBSDBCrypt; - import org.opensearch.OpenSearchSecurityException; +import org.opensearch.common.settings.Settings; import org.opensearch.security.auth.AuthenticationBackend; import org.opensearch.security.auth.AuthorizationBackend; +import org.opensearch.security.hash.PasswordHasher; +import org.opensearch.security.hash.PasswordHasherImpl; import org.opensearch.security.securityconf.InternalUsersModel; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.user.User; @@ -49,6 +50,11 @@ public class InternalAuthenticationBackend implements AuthenticationBackend, AuthorizationBackend { private InternalUsersModel internalUsersModel; + private final PasswordHasher passwordHasher; + + public InternalAuthenticationBackend(Settings settings){ + this.passwordHasher = new PasswordHasherImpl(settings); + } @Override public boolean exists(User user) { @@ -91,7 +97,7 @@ public boolean exists(User user) { * @return Whether the hash matches the provided password */ public boolean passwordMatchesHash(String hash, char[] array) { - return OpenBSDBCrypt.checkPassword(hash, array); + return passwordHasher.check(CharBuffer.wrap(array), hash); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java index 5d81dfa85d..164093d090 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java @@ -11,6 +11,7 @@ package org.opensearch.security.dlic.rest.api; +import java.nio.CharBuffer; import java.util.List; import java.util.Map; import java.util.Set; @@ -19,8 +20,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.commons.lang3.tuple.Triple; -import org.bouncycastle.crypto.generators.OpenBSDBCrypt; - import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.Strings; @@ -32,6 +31,8 @@ import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.dlic.rest.validation.ValidationResult; +import org.opensearch.security.hash.PasswordHasher; +import org.opensearch.security.hash.PasswordHasherImpl; import org.opensearch.security.securityconf.Hashed; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -44,13 +45,15 @@ import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.api.Responses.response; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; -import static org.opensearch.security.dlic.rest.support.Utils.hash; /** * Rest API action to fetch or update account details of the signed-in user. * Currently this action serves GET and PUT request for /_opendistro/_security/api/account endpoint */ public class AccountApiAction extends AbstractApiAction { + + private final PasswordHasher passwordHasher; + private static final List routes = addRoutesPrefix( ImmutableList.of(new Route(Method.GET, "/account"), new Route(Method.PUT, "/account")) ); @@ -62,6 +65,7 @@ public AccountApiAction( ) { super(Endpoint.ACCOUNT, clusterService, threadPool, securityApiDependencies); this.requestHandlersBuilder.configureRequestHandlers(this::accountApiRequestHandlers); + this.passwordHasher = new PasswordHasherImpl(securityApiDependencies.settings()); } @Override @@ -132,7 +136,7 @@ ValidationResult validCurrentPassword(final SecurityConfi final var currentPassword = content.get("current_password").asText(); final var internalUserEntry = (Hashed) securityConfiguration.configuration().getCEntry(username); final var currentHash = internalUserEntry.getHash(); - if (currentHash == null || !OpenBSDBCrypt.checkPassword(currentHash, currentPassword.toCharArray())) { + if (currentHash == null || !passwordHasher.check(CharBuffer.wrap(currentPassword.toCharArray()), currentHash)) { return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("Could not validate your current password.")); } return ValidationResult.success(securityConfiguration); @@ -148,7 +152,7 @@ ValidationResult updatePassword(final SecurityConfigurati if (Strings.isNullOrEmpty(password)) { hash = securityJsonNode.get("hash").asString(); } else { - hash = hash(password.toCharArray()); + hash = passwordHasher.hash(CharBuffer.wrap(password.toCharArray())); } if (Strings.isNullOrEmpty(hash)) { return ValidationResult.error( diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 3cbcc18bd9..d4417ad3a9 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java @@ -12,6 +12,7 @@ package org.opensearch.security.dlic.rest.api; import java.io.IOException; +import java.nio.CharBuffer; import java.util.List; import java.util.Map; @@ -31,6 +32,8 @@ import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.dlic.rest.validation.ValidationResult; +import org.opensearch.security.hash.PasswordHasher; +import org.opensearch.security.hash.PasswordHasherImpl; import org.opensearch.security.securityconf.Hashed; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -47,7 +50,6 @@ import static org.opensearch.security.dlic.rest.api.Responses.payload; import static org.opensearch.security.dlic.rest.api.Responses.response; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; -import static org.opensearch.security.dlic.rest.support.Utils.hash; public class InternalUsersApiAction extends AbstractApiAction { @@ -57,6 +59,8 @@ protected void consumeParameters(final RestRequest request) { request.param("filterBy"); } + private final PasswordHasher passwordHasher; + static final List RESTRICTED_FROM_USERNAME = ImmutableList.of( ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 ); @@ -92,6 +96,7 @@ public InternalUsersApiAction( super(Endpoint.INTERNALUSERS, clusterService, threadPool, securityApiDependencies); this.userService = userService; this.requestHandlersBuilder.configureRequestHandlers(this::internalUsersApiRequestHandlers); + this.passwordHasher = new PasswordHasherImpl(securityApiDependencies.settings()); } @Override @@ -268,7 +273,7 @@ private ValidationResult generateHashForPassword(final Se if (content.has("password")) { final var plainTextPassword = content.get("password").asText(); content.remove("password"); - content.put("hash", hash(plainTextPassword.toCharArray())); + content.put("hash", passwordHasher.hash(CharBuffer.wrap(plainTextPassword.toCharArray()))); } return ValidationResult.success(securityConfiguration); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index ee68a629c6..2ab5b03553 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -16,12 +16,10 @@ import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; -import java.security.SecureRandom; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import com.google.common.collect.ImmutableList; @@ -31,7 +29,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.tuple.Pair; -import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchParseException; @@ -54,6 +51,7 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; + import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; @@ -195,21 +193,6 @@ public static Map byteArrayToMutableJsonMap(byte[] jsonBytes) th } } - /** - * This generates hash for a given password - * @param clearTextPassword plain text password for which hash should be generated. - * This will be cleared from memory. - * @return hash of the password - */ - public static String hash(final char[] clearTextPassword) { - final byte[] salt = new byte[16]; - new SecureRandom().nextBytes(salt); - final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); - Arrays.fill(salt, (byte) 0); - Arrays.fill(clearTextPassword, '\0'); - return hash; - } - /** * Generate field resource paths * @param fields fields @@ -287,5 +270,4 @@ public static T withIOException(final CheckedSupplier action throw new UncheckedIOException(ioe); } } - } diff --git a/src/main/java/org/opensearch/security/hash/PasswordHasher.java b/src/main/java/org/opensearch/security/hash/PasswordHasher.java new file mode 100644 index 0000000000..d4741bf942 --- /dev/null +++ b/src/main/java/org/opensearch/security/hash/PasswordHasher.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.hash; + +import java.nio.CharBuffer; + +public interface PasswordHasher { + String hash(CharBuffer password); + boolean check(CharBuffer password, String hashedPassword); +} diff --git a/src/main/java/org/opensearch/security/hash/PasswordHasherImpl.java b/src/main/java/org/opensearch/security/hash/PasswordHasherImpl.java new file mode 100644 index 0000000000..64948a68e8 --- /dev/null +++ b/src/main/java/org/opensearch/security/hash/PasswordHasherImpl.java @@ -0,0 +1,146 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.hash; + +import com.password4j.*; +import com.password4j.types.Argon2; +import com.password4j.types.Bcrypt; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.support.ConfigConstants; + +import java.nio.CharBuffer; +import java.util.Arrays; + +import static org.opensearch.security.support.ConfigConstants.*; + +public class PasswordHasherImpl implements PasswordHasher { + + private final Settings settings; + + public PasswordHasherImpl(Settings settings) { + this.settings = settings; + } + + public PasswordHasherImpl(){ + this(Settings.EMPTY); + } + + + @Override + public String hash(CharBuffer password) { + try { + return Password + .hash(password) + .with(getHashingFunction()) + .getResult(); + } finally { + cleanup(password); + } + } + + @Override + public boolean check(CharBuffer password, String hash) { + try { + return Password + .check(password, hash) + .with(getHashingFunction()); + } finally { + cleanup(password); + } + } + + private void cleanup(CharBuffer password) { + password.clear(); + char[] passwordOverwrite = new char[password.capacity()]; + Arrays.fill(passwordOverwrite, '\0'); + password.put(passwordOverwrite); + } + + + private HashingFunction getHashingFunction() { + + String algorithm = settings.get(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT); + + HashingFunction hashingFunction; + + switch(algorithm.toLowerCase()){ + case BCRYPT: + hashingFunction = getBCryptFunction(); + break; + case PBKDF2: + hashingFunction = getPBKDF2Function(); + break; + case SCRYPT: + hashingFunction = getSCryptFunction(); + break; + case ARGON2: + hashingFunction = getArgon2Function(); + break; + default: + throw new OpenSearchSecurityException("Password hashing algorithm not supported"); + } + return hashingFunction; + } + + + private HashingFunction getPBKDF2Function() { + int iterations = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT); + int length = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT); + String pbkdf2Function = settings.get(ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT); + + return CompressedPBKDF2Function.getInstance(pbkdf2Function, iterations, length); + } + + private HashingFunction getSCryptFunction() { + int workFactor = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR_DEFAULT); + int resources = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES_DEFAULT); + int parallelization = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION_DEFAULT); + int derivedKeyLength = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH_DEFAULT); + return ScryptFunction.getInstance(workFactor, resources, parallelization, derivedKeyLength); + } + + private HashingFunction getArgon2Function() { + int memory = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT); + int iterations = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT); + int length = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT); + int parallelism = settings.getAsInt(SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT); + String type = settings.get(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT); + int version = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT); + return Argon2Function.getInstance( + memory, iterations, parallelism, length, Argon2.valueOf(type.toUpperCase()), version); + } + + private HashingFunction getBCryptFunction() { + int rounds = settings.getAsInt(ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS, + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT); + String minor = settings.get(ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR, + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT); + return BcryptFunction.getInstance(Bcrypt.valueOf(minor), rounds); + } + + +} diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index f046b4c114..e7a19d3ddf 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -128,7 +128,7 @@ public final static SecurityDynamicConfiguration addStatics(SecurityDynamicCo private final EventBus eventBus = EVENT_BUS_BUILDER.logger(new JavaLogger(DynamicConfigFactory.class.getCanonicalName())).build(); private final Settings opensearchSettings; private final Path configPath; - private final InternalAuthenticationBackend iab = new InternalAuthenticationBackend(); + private final InternalAuthenticationBackend iab; private final ClusterInfoHolder cih; SecurityDynamicConfiguration config; @@ -146,6 +146,7 @@ public DynamicConfigFactory( this.opensearchSettings = opensearchSettings; this.configPath = configPath; this.cih = cih; + this.iab = new InternalAuthenticationBackend(opensearchSettings); if (opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES, true)) { try { diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 9c671a80f9..221c251408 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.password4j.types.Hmac; import org.opensearch.common.settings.Settings; import org.opensearch.security.auditlog.impl.AuditCategory; @@ -144,6 +145,43 @@ public class ConfigConstants { public static final String SECURITY_AUTHCZ_IMPERSONATION_DN = "plugins.security.authcz.impersonation_dn"; public static final String SECURITY_AUTHCZ_REST_IMPERSONATION_USERS = "plugins.security.authcz.rest_impersonation_user"; + public static final String BCRYPT = "bcrypt"; + public static final String PBKDF2 = "pbkdf2"; + public static final String SCRYPT = "scrypt"; + public static final String ARGON2 = "argon2"; + public static final String SECURITY_PASSWORD_HASHING_ALGORITHM = "plugins.security.password.hashing.algorithm"; + public static final String SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT = BCRYPT; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS = "plugins.security.password.hashing.pbkdf2.iterations"; + public static final int SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT = 310000; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH = "plugins.security.password.hashing.pbkdf2.length"; + public static final int SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT = 512; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION = "plugins.security.password.hashing.pbkdf2.function"; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT = Hmac.SHA256.name(); + public static final String SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR = "plugins.security.password.hashing.scrypt.work_factor"; + public static final int SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR_DEFAULT = 65536; + public static final String SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES = "plugins.security.password.hashing.scrypt.resources"; + public static final int SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES_DEFAULT = 8; + public static final String SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION = "plugins.security.password.hashing.scrypt.parallelization"; + public static final int SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION_DEFAULT = 1; + public static final String SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH = "plugins.security.password.hashing.scrypt.derived_key_length"; + public static final int SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH_DEFAULT = 64; + public static final String SECURITY_PASSWORD_HASHING_ARGON2_MEMORY = "plugins.security.password.hashing.argon2.memory"; + public static final int SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT = 15360; + public static final String SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS = "plugins.security.password.hashing.argon2.iterations"; + public static final int SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT = 2; + public static final String SECURITY_PASSWORD_HASHING_ARGON2_LENGTH = "plugins.security.password.hashing.argon2.length"; + public static final int SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT = 32; + public static final String SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM = "plugins.security.password.hashing.argon2.parallelism"; + public static final int SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT = 1; + public static final String SECURITY_PASSWORD_HASHING_ARGON2_TYPE = "plugins.security.password.hashing.argon2.type"; + public static final String SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT = "id"; + public static final String SECURITY_PASSWORD_HASHING_ARGON2_VERSION = "plugins.security.password.hashing.argon2.version"; + public static final int SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT = 19; + public static final String SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS = "plugins.security.password.hashing.bcrypt.rounds"; + public static final int SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT = 12; + public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR = "plugins.security.password.hashing.bcrypt.minor"; + public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT = "B"; + public static final String SECURITY_AUDIT_TYPE_DEFAULT = "plugins.security.audit.type"; public static final String SECURITY_AUDIT_CONFIG_DEFAULT = "plugins.security.audit.config"; public static final String SECURITY_AUDIT_CONFIG_ROUTES = "plugins.security.audit.routes"; diff --git a/src/main/java/org/opensearch/security/tools/Hasher.java b/src/main/java/org/opensearch/security/tools/Hasher.java index f19d958523..a6ea9be252 100644 --- a/src/main/java/org/opensearch/security/tools/Hasher.java +++ b/src/main/java/org/opensearch/security/tools/Hasher.java @@ -27,53 +27,82 @@ package org.opensearch.security.tools; import java.io.Console; -import java.security.SecureRandom; -import java.util.Arrays; +import java.nio.CharBuffer; import java.util.Objects; -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; -import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.apache.commons.cli.*; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.hash.PasswordHasher; +import org.opensearch.security.hash.PasswordHasherImpl; +import org.opensearch.security.support.ConfigConstants; + +import static org.opensearch.security.support.ConfigConstants.*; public class Hasher { - public static void main(final String[] args) { + private static final String PASSWORD_OPTION = "p"; + private static final String ENV_OPTION = "env"; + private static final String ALGORITHM_OPTION = "a"; + private static final String ROUNDS_OPTION = "r"; + private static final String MINOR_OPTION = "min"; + private static final String VERSION_OPTION = "v"; + private static final String TYPE_OPTION = "t"; + private static final String PARALLELISM_OPTION = "prl"; + private static final String LENGTH_OPTION = "l"; + private static final String ITERATIONS_OPTION = "i"; + private static final String MEMORY_OPTION = "mem"; + private static final String DERIVED_KEY_LENGTH_OPTION = "d"; + private static final String RESOURCES_OPTION = "res"; + private static final String WORK_FACTOR_OPTION = "wf"; + private static final String FUNCTION_OPTION = "f"; - final Options options = new Options(); + public static void main(final String[] args) { final HelpFormatter formatter = new HelpFormatter(); - options.addOption(Option.builder("p").argName("password").hasArg().desc("Cleartext password to hash").build()); - options.addOption( - Option.builder("env") - .argName("name environment variable") - .hasArg() - .desc("name environment variable to read password from") - .build() - ); - + Options options = buildOptions(); final CommandLineParser parser = new DefaultParser(); try { final CommandLine line = parser.parse(options, args); + final char[] password; - if (line.hasOption("p")) { - System.out.println(hash(line.getOptionValue("p").toCharArray())); - } else if (line.hasOption("env")) { - final String pwd = System.getenv(line.getOptionValue("env")); + if (line.hasOption(PASSWORD_OPTION)) { + password = line.getOptionValue(PASSWORD_OPTION).toCharArray(); + } else if (line.hasOption(ENV_OPTION)) { + final String pwd = System.getenv(line.getOptionValue(ENV_OPTION)); if (pwd == null || pwd.isEmpty()) { - throw new Exception("No environment variable '" + line.getOptionValue("env") + "' set"); + throw new Exception("No environment variable '" + line.getOptionValue(ENV_OPTION) + "' set"); } - System.out.println(hash(pwd.toCharArray())); + password = pwd.toCharArray(); } else { final Console console = System.console(); if (console == null) { throw new Exception("Cannot allocate a console"); } - final char[] passwd = console.readPassword("[%s]", "Password:"); - System.out.println(hash(passwd)); + password = console.readPassword("[%s]", "Password:"); } + if(line.hasOption(ALGORITHM_OPTION)){ + String algorithm = line.getOptionValue(ALGORITHM_OPTION); + Settings settings; + switch (algorithm.toLowerCase()) { + case BCRYPT: + settings = getBCryptSettings(line); + break; + case PBKDF2: + settings = getPBKDF2Settings(line); + break; + case SCRYPT: + settings = getSCryptSettings(line); + break; + case ARGON2: + settings = getArgon2Settings(line); + break; + default: + throw new Exception("Unsupported hashing algorithm: " + algorithm); + } + System.out.println(hash(password, settings)); + }else { + System.out.println(hash(password)); + } + } catch (final Exception exp) { System.err.println("Parsing failed. Reason: " + exp.getMessage()); formatter.printHelp("hash.sh", options, true); @@ -82,11 +111,202 @@ public static void main(final String[] args) { } public static String hash(final char[] clearTextPassword) { - final byte[] salt = new byte[16]; - new SecureRandom().nextBytes(salt); - final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); - Arrays.fill(salt, (byte) 0); - Arrays.fill(clearTextPassword, '\0'); - return hash; + return hash(clearTextPassword, Settings.EMPTY); + } + + private static String hash(final char[] clearTextPassword, final String algorithm){ + Settings settings = Settings.builder().put( + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, algorithm) + .build(); + return hash(clearTextPassword, settings); + } + + private static String hash(final char[] clearTextPassword, Settings settings){ + PasswordHasher passwordHasher = new PasswordHasherImpl(settings); + return passwordHasher.hash((Objects.requireNonNull(CharBuffer.wrap(clearTextPassword)))); + } + + private static Options buildOptions(){ + final Options options = new Options(); + options.addOption( + Option.builder(PASSWORD_OPTION) + .argName("password") + .hasArg() + .desc("Cleartext password to hash") + .build()); + options.addOption( + Option.builder(ENV_OPTION) + .argName("name environment variable") + .hasArg() + .desc("name environment variable to read password from") + .build() + ); + options.addOption( + Option.builder(ALGORITHM_OPTION) + .longOpt("algorithm") + .argName("hashing algorithm") + .hasArg() + .desc("Hashing algorithm (BCrypt, PBKDF2, SCrypt, Argon2)") + .build() + ); + options.addOption( + Option.builder(ROUNDS_OPTION) + .longOpt("rounds") + .desc("Number of rounds (for BCrypt).") + .hasArg() + .argName("rounds") + .type(Number.class) + .build()); + options.addOption( + Option.builder(MINOR_OPTION) + .longOpt("minor") + .desc("Minor version (for BCrypt).") + .hasArg() + .argName("minor") + .build()); + options.addOption( + Option.builder(VERSION_OPTION) + .longOpt("version") + .desc("Version of the algorithm (for Argon2).") + .hasArg() + .argName("version") + .type(Number.class) + .build()); + options.addOption( + Option.builder(TYPE_OPTION) + .longOpt("type") + .desc("Type of the algorithm (for Argon2).") + .hasArg() + .argName("type") + .build()); + options.addOption( + Option.builder(PARALLELISM_OPTION) + .longOpt("parallelism") + .desc("Degree of parallelism (for Argon2, SCrypt).") + .hasArg() + .argName("parallelism") + .type(Number.class) + .build()); + options.addOption( + Option.builder(LENGTH_OPTION) + .longOpt("length") + .desc("Desired length of the final derived key (for Argon2, PBKDF2).") + .hasArg() + .argName("length") + .type(Number.class) + .build()); + options.addOption( + Option.builder(ITERATIONS_OPTION) + .longOpt("iterations") + .desc("Iterations to perform (for Argon2, PBKDF2).") + .hasArg() + .argName("iterations") + .type(Number.class) + .build()); + options.addOption( + Option.builder(MEMORY_OPTION) + .longOpt("memory") + .desc("Memory to use (for Argon2).") + .hasArg() + .argName("memory") + .type(Number.class) + .build()); + options.addOption( + Option.builder(DERIVED_KEY_LENGTH_OPTION) + .longOpt("derived-key-length") + .desc("Length of the derived key (for SCrypt).") + .hasArg() + .argName("length") + .type(Number.class) + .build()); + options.addOption( + Option.builder(RESOURCES_OPTION) + .longOpt("resources") + .desc("Resources (for SCrypt).") + .hasArg() + .argName("resources") + .type(Number.class) + .build()); + options.addOption( + Option.builder(WORK_FACTOR_OPTION) + .longOpt("work-factor") + .desc("Work factor (for SCrypt).") + .hasArg() + .argName("work-factor") + .build()); + options.addOption( + Option.builder(FUNCTION_OPTION) + .longOpt("function") + .desc("Pseudo-random function applied to the password (for PBKDF2).") + .hasArg() + .argName("function") + .build()); + return options; + } + + private static Settings getBCryptSettings(CommandLine line) throws ParseException{ + Settings.Builder settings = Settings.builder(); + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, BCRYPT); + if(line.hasOption(ROUNDS_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS, ((Number)line.getParsedOptionValue(ROUNDS_OPTION)).intValue()); + } + if(line.hasOption(MINOR_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR, line.getOptionValue(MINOR_OPTION).toUpperCase()); + } + return settings.build(); + } + + private static Settings getPBKDF2Settings(CommandLine line) throws ParseException{ + Settings.Builder settings = Settings.builder(); + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, PBKDF2); + if (line.hasOption(FUNCTION_OPTION)) { + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION, line.getOptionValue(FUNCTION_OPTION)); + } + if (line.hasOption(LENGTH_OPTION)) { + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH, ((Number)line.getParsedOptionValue(LENGTH_OPTION)).intValue()); + } + if (line.hasOption(ITERATIONS_OPTION)) { + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS, ((Number)line.getParsedOptionValue(ITERATIONS_OPTION)).intValue()); + } + return settings.build(); + } + + private static Settings getSCryptSettings(CommandLine line) throws ParseException{ + Settings.Builder settings = Settings.builder(); + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, SCRYPT); + if(line.hasOption(DERIVED_KEY_LENGTH_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_DERIVED_KEY_LENGTH, ((Number)line.getParsedOptionValue(DERIVED_KEY_LENGTH_OPTION)).intValue()); + } + if(line.hasOption(PARALLELISM_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION, ((Number)line.getParsedOptionValue(PARALLELISM_OPTION)).intValue()); + } + if(line.hasOption(RESOURCES_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES, ((Number)line.getParsedOptionValue(RESOURCES_OPTION)).intValue()); + } + return settings.build(); + } + + private static Settings getArgon2Settings(CommandLine line) throws ParseException{ + Settings.Builder settings = Settings.builder(); + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ARGON2); + if(line.hasOption(VERSION_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION, ((Number)line.getParsedOptionValue(VERSION_OPTION)).intValue()); + } + if(line.hasOption(TYPE_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE, line.getOptionValue(TYPE_OPTION)); + } + if(line.hasOption(PARALLELISM_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM, ((Number)line.getParsedOptionValue(PARALLELISM_OPTION)).intValue()); + } + if(line.hasOption(LENGTH_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH, ((Number)line.getParsedOptionValue(LENGTH_OPTION)).intValue()); + } + if(line.hasOption(ITERATIONS_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS, ((Number)line.getParsedOptionValue(ITERATIONS_OPTION)).intValue()); + } + if(line.hasOption(MEMORY_OPTION)){ + settings.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY, ((Number)line.getParsedOptionValue(MEMORY_OPTION)).intValue()); + } + return settings.build(); } } diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index 937a5331a8..f8ea9c09ad 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -12,6 +12,7 @@ package org.opensearch.security.user; import java.io.IOException; +import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -45,6 +46,8 @@ import org.opensearch.identity.tokens.BasicAuthToken; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.hash.PasswordHasher; +import org.opensearch.security.hash.PasswordHasherImpl; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.Hashed; import org.opensearch.security.securityconf.impl.CType; @@ -57,7 +60,6 @@ import org.passay.EnglishCharacterData; import org.passay.PasswordGenerator; -import static org.opensearch.security.dlic.rest.support.Utils.hash; /** * This class handles user registration and operations on behalf of the Security Plugin. @@ -69,6 +71,8 @@ public class UserService { private final ConfigurationRepository configurationRepository; String securityIndex; Client client; + Settings settings; + private final PasswordHasher passwordHasher; User tokenUser; final static String NO_PASSWORD_OR_HASH_MESSAGE = "Please specify either 'hash' or 'password' when creating a new internal user."; @@ -102,6 +106,8 @@ public UserService(ClusterService clusterService, ConfigurationRepository config ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); this.client = client; + this.settings = settings; + this.passwordHasher = new PasswordHasherImpl(settings); } /** @@ -142,7 +148,7 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA // service account verifyServiceAccount(securityJsonNode, accountName); String password = generatePassword(); - contentAsNode.put("hash", hash(password.toCharArray())); + contentAsNode.put("hash", passwordHasher.hash(CharBuffer.wrap(password.toCharArray()))); contentAsNode.put("service", "true"); } else { contentAsNode.put("service", "false"); @@ -162,7 +168,7 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA final String origHash = securityJsonNode.get("hash").asString(); if (plainTextPassword != null && plainTextPassword.length() > 0) { contentAsNode.remove("password"); - contentAsNode.put("hash", hash(plainTextPassword.toCharArray())); + contentAsNode.put("hash", passwordHasher.hash(CharBuffer.wrap(plainTextPassword.toCharArray()))); } else if (origHash != null && origHash.length() > 0) { contentAsNode.remove("password"); } else if (plainTextPassword != null && plainTextPassword.isEmpty() && origHash == null) { @@ -275,7 +281,7 @@ public AuthToken generateAuthToken(String accountName) throws IOException { // Generate a new password for the account and store the hash of it String plainTextPassword = generatePassword(); - contentAsNode.put("hash", hash(plainTextPassword.toCharArray())); + contentAsNode.put("hash", passwordHasher.hash(CharBuffer.wrap(plainTextPassword.toCharArray()))); contentAsNode.put("enabled", "true"); contentAsNode.put("service", "true"); diff --git a/src/test/java/org/opensearch/security/PasswordHasherTest.java b/src/test/java/org/opensearch/security/PasswordHasherTest.java new file mode 100644 index 0000000000..a680fc0605 --- /dev/null +++ b/src/test/java/org/opensearch/security/PasswordHasherTest.java @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security; + +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.junit.Test; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.hash.PasswordHasher; +import org.opensearch.security.hash.PasswordHasherImpl; +import org.opensearch.security.support.ConfigConstants; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.nio.CharBuffer; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Objects; + +public class PasswordHasherTest { + + final String password = "testPassword"; + final String wrongPassword = "wrongTestPassword"; + + @Test + public void testDefaultHash(){ + //should default to BCrypt + final Settings settings = Settings.EMPTY; + PasswordHasher passwordHasher = new PasswordHasherImpl(settings); + String hashedPassword = passwordHasher.hash(CharBuffer.wrap(password.toCharArray())); + assertTrue(passwordHasher.check(CharBuffer.wrap(password.toCharArray()), hashedPassword)); + assertFalse(passwordHasher.check(CharBuffer.wrap(wrongPassword.toCharArray()), hashedPassword)); + } + + @Test + public void testLegacyHash(){ + final Settings settings = Settings.EMPTY; + PasswordHasher passwordHasher = new PasswordHasherImpl(settings); + String legacyHash = generateLegacyBCryptHash(password.toCharArray()); + assertTrue(passwordHasher.check(CharBuffer.wrap(password.toCharArray()), legacyHash)); + assertFalse(passwordHasher.check(CharBuffer.wrap(wrongPassword.toCharArray()), legacyHash)); + } + + @Test + public void testPKDF2(){ + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "pbkdf2") + .build(); + PasswordHasher passwordHasher = new PasswordHasherImpl(settings); + String hashedPassword = passwordHasher.hash(CharBuffer.wrap(password.toCharArray())); + assertTrue(passwordHasher.check(CharBuffer.wrap(password.toCharArray()), hashedPassword)); + assertFalse(passwordHasher.check(CharBuffer.wrap(wrongPassword.toCharArray()), hashedPassword)); + } + + @Test + public void testSCrypt(){ + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "scrypt") + .build(); + PasswordHasher passwordHasher = new PasswordHasherImpl(settings); + String hashedPassword = passwordHasher.hash(CharBuffer.wrap(password.toCharArray())); + assertTrue(passwordHasher.check(CharBuffer.wrap(password.toCharArray()), hashedPassword)); + assertFalse(passwordHasher.check(CharBuffer.wrap(wrongPassword.toCharArray()), hashedPassword)); + } + + @Test + public void testArgon2(){ + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "argon2") + .build(); + PasswordHasher passwordHasher = new PasswordHasherImpl(settings); + String hashedPassword = passwordHasher.hash(CharBuffer.wrap(password.toCharArray())); + assertTrue(passwordHasher.check(CharBuffer.wrap(password.toCharArray()), hashedPassword)); + assertFalse(passwordHasher.check(CharBuffer.wrap(wrongPassword.toCharArray()), hashedPassword)); + } + + @Test + public void testPasswordCleanup(){ + final Settings settings = Settings.EMPTY; + PasswordHasher passwordHasher = new PasswordHasherImpl(settings); + char[] password = new char[]{'p','a','s','s','w','o','r','d'}; + CharBuffer passwordBuffer = CharBuffer.wrap(password); + passwordHasher.hash(passwordBuffer); + assertFalse(new String(password).contains("password")); + assertFalse(passwordBuffer.toString().contains("password")); + } + + @Test(expected = OpenSearchSecurityException.class) + public void testWrongHashingAlgorithm(){ + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "unsupported") + .build(); + PasswordHasher passwordHasher = new PasswordHasherImpl(settings); + passwordHasher.hash(CharBuffer.wrap(password.toCharArray())); + } + + + private String generateLegacyBCryptHash(final char[] clearTextPassword){ + final byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); + Arrays.fill(salt, (byte) 0); + Arrays.fill(clearTextPassword, '\0'); + return hash; + } +} diff --git a/src/test/java/org/opensearch/security/UtilTests.java b/src/test/java/org/opensearch/security/UtilTests.java index 402d5dc92f..903136c93a 100644 --- a/src/test/java/org/opensearch/security/UtilTests.java +++ b/src/test/java/org/opensearch/security/UtilTests.java @@ -115,7 +115,7 @@ public void testEnvReplace() { assertEquals("abvtTtxyz", SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTt}xyz", settings)); assertTrue(OpenBSDBCrypt.checkPassword(SecurityUtils.replaceEnvVars("${envbc.MYENV:-tTt}", settings), "tTt".toCharArray())); assertEquals("abvtTtxyzxxx", SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTt}xyz${env.MYENV:-xxx}", settings)); - assertTrue(SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTt}xyz${envbc.MYENV:-xxx}", settings).startsWith("abvtTtxyz$2y$")); + assertTrue(SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTt}xyz${envbc.MYENV:-xxx}", settings).startsWith("abvtTtxyz$2b$")); assertEquals("abv${env.MYENV:tTt}xyz", SecurityUtils.replaceEnvVars("abv${env.MYENV:tTt}xyz", settings)); assertEquals("abv${env.MYENV-tTt}xyz", SecurityUtils.replaceEnvVars("abv${env.MYENV-tTt}xyz", settings)); // assertEquals("abvabcdefgxyz", SecurityUtils.replaceEnvVars("abv${envbase64.B64TEST}xyz",settings)); @@ -155,7 +155,7 @@ public void testNoEnvReplace() { "abv${env.MYENV:-tTt}xyz${env.MYENV:-xxx}", SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTt}xyz${env.MYENV:-xxx}", settings) ); - assertFalse(SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTt}xyz${envbc.MYENV:-xxx}", settings).startsWith("abvtTtxyz$2y$")); + assertFalse(SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTt}xyz${envbc.MYENV:-xxx}", settings).startsWith("abvtTtxyz$2b$")); assertEquals("abv${env.MYENV:tTt}xyz", SecurityUtils.replaceEnvVars("abv${env.MYENV:tTt}xyz", settings)); assertEquals("abv${env.MYENV-tTt}xyz", SecurityUtils.replaceEnvVars("abv${env.MYENV-tTt}xyz", settings)); Map env = System.getenv(); diff --git a/src/test/java/org/opensearch/security/auth/InternalAuthBackendTests.java b/src/test/java/org/opensearch/security/auth/InternalAuthBackendTests.java index b9503e79f1..24420fd9d0 100644 --- a/src/test/java/org/opensearch/security/auth/InternalAuthBackendTests.java +++ b/src/test/java/org/opensearch/security/auth/InternalAuthBackendTests.java @@ -21,6 +21,7 @@ import org.junit.Test; import org.opensearch.OpenSearchSecurityException; +import org.opensearch.common.settings.Settings; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; import org.opensearch.security.securityconf.InternalUsersModel; import org.opensearch.security.user.AuthCredentials; @@ -42,7 +43,7 @@ public class InternalAuthBackendTests { @Before public void internalAuthBackendTestsSetup() { - internalAuthenticationBackend = spy(new InternalAuthenticationBackend()); + internalAuthenticationBackend = spy(new InternalAuthenticationBackend(Settings.EMPTY)); internalUsersModel = mock(InternalUsersModel.class); internalAuthenticationBackend.onInternalUsersModelChanged(internalUsersModel); } diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DfmOverwritesAllTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DfmOverwritesAllTest.java index 580cabc66b..0e0c838baa 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DfmOverwritesAllTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DfmOverwritesAllTest.java @@ -219,7 +219,8 @@ public void testDFMRestrictedAndUnrestrictedAllIndices() throws Exception { */ @Test public void testDFMRestrictedAndUnrestrictedOneIndex() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, true).build(); + final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, true) + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT).build(); setup( settings, new DynamicSecurityConfig().setConfig("securityconfig_dfm_empty_overwrites_all.yml") diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java index 4b2e9e4417..99636e5df4 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java @@ -28,6 +28,8 @@ import org.opensearch.core.xcontent.ToXContent; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.hash.PasswordHasher; +import org.opensearch.security.hash.PasswordHasherImpl; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.threadpool.ThreadPool; @@ -62,6 +64,8 @@ public abstract class AbstractApiActionValidationTest { ObjectMapper objectMapper = DefaultObjectMapper.objectMapper; + PasswordHasher passwordHasher; + @Before public void setup() { securityApiDependencies = new SecurityApiDependencies( @@ -73,6 +77,8 @@ public void setup() { null, Settings.EMPTY ); + + passwordHasher = new PasswordHasherImpl(securityApiDependencies.settings()); } void setupRolesConfiguration() throws IOException { diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiActionConfigValidationsTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiActionConfigValidationsTest.java index 8af780e01a..79488b95f8 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiActionConfigValidationsTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiActionConfigValidationsTest.java @@ -14,11 +14,12 @@ import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.opensearch.core.rest.RestStatus; -import org.opensearch.security.dlic.rest.support.Utils; import org.opensearch.security.securityconf.impl.v7.InternalUserV7; import org.mockito.Mockito; +import java.nio.CharBuffer; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -35,7 +36,7 @@ public void verifyValidCurrentPassword() { assertFalse(result.isValid()); assertEquals(RestStatus.BAD_REQUEST, result.status()); - u.setHash(Utils.hash("aaaa".toCharArray())); + u.setHash(passwordHasher.hash(CharBuffer.wrap("aaaa".toCharArray()))); result = accountApiAction.validCurrentPassword(SecurityConfiguration.of(requestContent(), "u", configuration)); assertTrue(result.isValid()); } @@ -59,7 +60,7 @@ public void updatePassword() { assertTrue(OpenBSDBCrypt.checkPassword(u.getHash(), "cccccc".toCharArray())); requestContent.remove("password"); - requestContent.put("hash", Utils.hash("dddddd".toCharArray())); + requestContent.put("hash", passwordHasher.hash(CharBuffer.wrap("dddddd".toCharArray()))); result = accountApiAction.updatePassword(SecurityConfiguration.of(requestContent, "u", configuration)); assertTrue(result.isValid()); assertTrue(OpenBSDBCrypt.checkPassword(u.getHash(), "dddddd".toCharArray())); @@ -71,7 +72,7 @@ private ObjectNode requestContent() { private InternalUserV7 createExistingUser() { final var u = new InternalUserV7(); - u.setHash(Utils.hash("sssss".toCharArray())); + u.setHash(passwordHasher.hash(CharBuffer.wrap("sssss".toCharArray()))); Mockito.when(configuration.getCEntry("u")).thenReturn(u); return u; } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java index 2af598f5d5..4ffdbec2a5 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java @@ -12,12 +12,12 @@ package org.opensearch.security.dlic.rest.api; import java.io.IOException; +import java.nio.CharBuffer; import java.util.List; import java.util.Map; import org.junit.Before; import org.junit.Test; -import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestRequest; @@ -98,7 +98,7 @@ public void replacePasswordWithHash() throws Exception { assertEquals(RestStatus.OK, result.status()); assertFalse(securityConfiguration.requestContent().has("password")); assertTrue(securityConfiguration.requestContent().has("hash")); - assertTrue(OpenBSDBCrypt.checkPassword(securityConfiguration.requestContent().get("hash").asText(), "aaaaaa".toCharArray())); + assertTrue(passwordHasher.check(CharBuffer.wrap("aaaaaa".toCharArray()), securityConfiguration.requestContent().get("hash").asText())); } @Test diff --git a/src/test/java/org/opensearch/security/tools/HasherTests.java b/src/test/java/org/opensearch/security/tools/HasherTests.java new file mode 100644 index 0000000000..a3c647f768 --- /dev/null +++ b/src/test/java/org/opensearch/security/tools/HasherTests.java @@ -0,0 +1,172 @@ + package org.opensearch.security.tools; + +import com.password4j.CompressedPBKDF2Function; +import com.password4j.types.Hmac; +import org.junit.After; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Objects; + +public class HasherTests { + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final InputStream originalIn = System.in; + + @Before + public void setOutputStreams(){ + System.setOut(new PrintStream(out)); + } + + @After + public void restoreStreams(){ + System.setOut(originalOut); + System.setIn(originalIn); + } + + @Test + public void testWithDefaultArguments(){ + Hasher.main(new String[]{"-p", "password"}); + String hash = getHashFromStdOut(); + assertTrue("should return a valid BCrypt hash with the default BCrypt configuration", + hash.startsWith("$2b$12")); + } + + @Test + public void testWithBCryptRoundsArgument(){ + Hasher.main(new String[]{"-p", "password", "-a", "BCrypt", "-r", "5"}); + String hash = getHashFromStdOut(); + assertTrue("should return a valid BCrypt hash with the correct value for \"rounds\"", + hash.startsWith("$2b$05")); + out.reset(); + + Hasher.main(new String[]{"-p", "password", "-a", "BCrypt", "-r", "5"}); + hash = getHashFromStdOut(); + assertTrue("should return a valid BCrypt hash with the correct value for \"rounds\"", + hash.startsWith("$2b$05")); + } + + @Test + public void testWithBCryptMinorArgument(){ + Hasher.main(new String[]{"-p", "password", "-a", "BCrypt", "-min", "A"}); + String hash = getHashFromStdOut(); + assertTrue("should return a valid BCrypt hash with the correct value for \"minor\"", + hash.startsWith("$2a$12")); + out.reset(); + + Hasher.main(new String[]{"-p", "password", "-a", "BCrypt", "-min", "Y"}); + hash = getHashFromStdOut(); + assertTrue("should return a valid BCrypt hash with the correct value for \"minor\"", + hash.startsWith("$2y$12")); + out.reset(); + + Hasher.main(new String[]{"-p", "password", "-a", "BCrypt", "-min", "B"}); + hash = getHashFromStdOut(); + assertTrue("should return a valid BCrypt hash with the correct value for \"minor\"", + hash.startsWith("$2b$12")); + out.reset(); + } + + @Test + public void testWithBCryptAllArguments(){ + Hasher.main(new String[]{"-p", "password", "-a", "BCrypt", "-min", "A", "-r", "5"}); + String hash = getHashFromStdOut(); + assertTrue("should return a valid BCrypt hash with the correct configuration", + hash.startsWith("$2a$05")); + } + + @Test + public void testWithPBKDF2DefaultArguments(){ + Hasher.main(new String[]{"-p", "password", "-a", "PBKDF2"}); + String hash = getHashFromStdOut(); + CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(hash); + assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); + assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 310000); + assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 512); + } + + @Test + public void testWithPBKDF2FunctionArgument(){ + Hasher.main(new String[]{"-p", "password", "-a", "PBKDF2", "-f", "SHA512"}); + String hash = getHashFromStdOut(); + CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(hash); + assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA512"); + assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 310000); + assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 512); + out.reset(); + + Hasher.main(new String[]{"-p", "password", "-a", "PBKDF2", "-f", "SHA384"}); + hash = getHashFromStdOut(); + pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(hash); + assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA384"); + assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 310000); + assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 512); + } + + @Test + public void testWithPBKDF2IterationsArgument(){ + Hasher.main(new String[]{"-p", "password", "-a", "PBKDF2", "-i", "100000"}); + String hash = getHashFromStdOut(); + CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(hash); + assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); + assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 100000); + assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 512); + out.reset(); + + Hasher.main(new String[]{"-p", "password", "-a", "PBKDF2", "-i", "200000"}); + hash = getHashFromStdOut(); + pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(hash); + assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); + assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 200000); + assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 512); + } + + @Test + public void testWithPBKDF2LengthArgument(){ + Hasher.main(new String[]{"-p", "password", "-a", "PBKDF2", "-l", "400"}); + String hash = getHashFromStdOut(); + CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(hash); + assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); + assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 310000); + assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 400); + out.reset(); + + Hasher.main(new String[]{"-p", "password", "-a", "PBKDF2", "-l", "300"}); + hash = getHashFromStdOut(); + pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(hash); + assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); + assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 310000); + assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 300); + } + + @Test + public void testWithPBKDF2AllArguments(){ + Hasher.main(new String[]{"-p", "password", "-a", "PBKDF2", "-l", "250", "-i", "150000", "-f", "SHA384"}); + String hash = getHashFromStdOut(); + CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(hash); + assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA384"); + assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 150000); + assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 250); + } + + @Test + public void testWithSCryptDefaultArguments(){ + Hasher.main(new String[]{"-p", "password", "-a", "SCrypt"}); + String hash = getHashFromStdOut(); + assertEquals("should return a valid SCrypt hash with the correct value for \"derived key length\"", "a", "a"); + } + + + private String getHashFromStdOut(){ + String[] splitOut = out.toString().split("\n"); + return splitOut[splitOut.length-1]; + } + +}