diff --git a/build.gradle b/build.gradle index a4e4a7d037..7df09e54d0 100644 --- a/build.gradle +++ b/build.gradle @@ -581,6 +581,8 @@ dependencies { implementation 'org.ldaptive:ldaptive:1.2.3' implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3' + 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 d4e1fe6998..5db1a7ef97 100644 --- a/plugin-security.policy +++ b/plugin-security.policy @@ -77,6 +77,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..0bcd669e03 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java @@ -12,10 +12,13 @@ import org.junit.Test; +import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.ToXContentObject; 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; @@ -110,7 +113,7 @@ private void verifyPasswordCanBeChanged() throws Exception { TEST_USER, TEST_USER_PASSWORD, client -> ok( - () -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, hash(newPassword.toCharArray()))) + () -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, hash(CharBuffer.wrap(newPassword.toCharArray()), Settings.EMPTY))) ) ); withUser( diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 79f10a76cf..1b0ec0d59f 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,17 +47,18 @@ 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.dlic.rest.support.Utils; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; @@ -451,7 +453,7 @@ public Object getAttribute(String attributeName) { public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { xContentBuilder.startObject(); - xContentBuilder.field("hash", hash(password.toCharArray())); + xContentBuilder.field("hash", hash(password)); Set roleNames = getRoleNames(); @@ -933,13 +935,11 @@ 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 hash(final String clearTextPassword) { + return Utils.hash(( + Objects.requireNonNull( + CharBuffer.wrap(clearTextPassword.toCharArray()) + )), Settings.EMPTY); } 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..e71eb4b5b6 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1916,6 +1916,140 @@ public List> getSettings() { ); } + + //todo: What is Property.Filtered? + //todo: Do we want these properties to be Final? + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + ) + ); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.simpleString( + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ALGORITHM, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ALGORITHM_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_WORK_FACTOR_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_RESOURCES_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION, + ConfigConstants.SECURITY_PASSWORD_HASHING_SCRYPT_PARALLELIZATION_DEFAULT, + Property.NodeScope, + Property.Filtered, + 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.Filtered, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.simpleString( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION, + ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + + settings.add(Setting.intSetting( + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS, + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + + settings.add(Setting.simpleString( + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR, + ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + )); + return settings; } 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..cf70f9da77 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; @@ -28,6 +27,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest.Method; +import org.opensearch.security.dlic.rest.support.Utils; import org.opensearch.security.dlic.rest.validation.EndpointValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; @@ -132,7 +132,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 || !Utils.checkPassword(CharBuffer.wrap(currentPassword.toCharArray()), currentHash, this.securityApiDependencies.settings())) { return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("Could not validate your current password.")); } return ValidationResult.success(securityConfiguration); @@ -148,7 +148,7 @@ ValidationResult updatePassword(final SecurityConfigurati if (Strings.isNullOrEmpty(password)) { hash = securityJsonNode.get("hash").asString(); } else { - hash = hash(password.toCharArray()); + hash = hash(CharBuffer.wrap(password.toCharArray()), this.securityApiDependencies.settings()); } 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..f961ec0e9c 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; @@ -268,7 +269,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", hash(CharBuffer.wrap(plainTextPassword.toCharArray()), securityApiDependencies.settings())); } 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..f1171ab063 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 @@ -13,15 +13,14 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.CharBuffer; 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; @@ -30,13 +29,16 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.password4j.*; +import com.password4j.types.Argon2; +import com.password4j.types.Bcrypt; import org.apache.commons.lang3.tuple.Pair; -import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchParseException; import org.opensearch.SpecialPermission; import org.opensearch.common.CheckedSupplier; +import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; @@ -54,6 +56,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,19 +198,25 @@ 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; + public static String hash(CharBuffer password, final Settings settings) { + try { + return Password + .hash(password) + .with(getHashingFunction(settings)) + .getResult(); + } finally { + cleanup(password); + } + } + + public static boolean checkPassword(CharBuffer password, final String hash, final Settings settings) { + try { + return Password + .check(password, hash) + .with(getHashingFunction(settings)); + } finally { + cleanup(password); + } } /** @@ -288,4 +297,83 @@ public static T withIOException(final CheckedSupplier action } } + private static HashingFunction getHashingFunction(Settings settings) { + + String algorithm = settings.get(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT); + + HashingFunction hashingFunction; + switch(algorithm){ + case "pbkdf2": + hashingFunction = Utils.getPBKDF2Function(settings); + break; + case "scrypt": + hashingFunction = Utils.getSCryptFunction(settings); + break; + case "argon2": + hashingFunction = Utils.getArgon2Function(settings); + break; + default: case "bcrypt": + hashingFunction = Utils.getBCryptFunction(settings); + break; + } + return hashingFunction; + } + + private static HashingFunction getPBKDF2Function(Settings settings) { + 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 pbkdf2Algorithm = settings.get(ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ALGORITHM, + ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ALGORITHM_DEFAULT); + return PBKDF2Function.getInstance(pbkdf2Algorithm, iterations, length); + } + + private static HashingFunction getSCryptFunction(Settings settings) { + 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); + //derivedKeyLength is not necesary + return ScryptFunction.getInstance(workFactor, resources, parallelization, derivedKeyLength); + } + + private static HashingFunction getArgon2Function(Settings settings) { + 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(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH, + 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 static HashingFunction getBCryptFunction(Settings settings) { + 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); + } + + private static void cleanup(CharBuffer password) { + password.clear(); + char[] passwordOverwrite = new char[password.capacity()]; + Arrays.fill(passwordOverwrite, '\0'); + password.put(passwordOverwrite); + password.clear(); + } + } diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 5169d02d20..580b13a6c3 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -145,6 +145,39 @@ 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 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_ALGORITHM = "plugins.security.password.hashing.pbkdf2.algorithm"; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_ALGORITHM_DEFAULT = "PBKDF2WithHmacSHA256"; + 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 = 10; + 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..6537c37fcd 100644 --- a/src/main/java/org/opensearch/security/tools/Hasher.java +++ b/src/main/java/org/opensearch/security/tools/Hasher.java @@ -27,8 +27,7 @@ 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; @@ -37,7 +36,8 @@ 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.opensearch.common.settings.Settings; +import org.opensearch.security.dlic.rest.support.Utils; public class Hasher { @@ -82,11 +82,9 @@ 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; + //hack for now to get tests running + //todo: convert to byte[] array if I can't use char[] with log4j + String password = new String(clearTextPassword); + return Utils.hash((Objects.requireNonNull(CharBuffer.wrap(password.toCharArray()))), Settings.Builder.EMPTY_SETTINGS); } } diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index 937a5331a8..a00a9b5a46 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; @@ -69,6 +70,7 @@ public class UserService { private final ConfigurationRepository configurationRepository; String securityIndex; Client client; + Settings settings; User tokenUser; final static String NO_PASSWORD_OR_HASH_MESSAGE = "Please specify either 'hash' or 'password' when creating a new internal user."; @@ -102,6 +104,7 @@ public UserService(ClusterService clusterService, ConfigurationRepository config ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); this.client = client; + this.settings = settings; } /** @@ -142,7 +145,7 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA // service account verifyServiceAccount(securityJsonNode, accountName); String password = generatePassword(); - contentAsNode.put("hash", hash(password.toCharArray())); + contentAsNode.put("hash", hash(CharBuffer.wrap(password.toCharArray()), this.settings)); contentAsNode.put("service", "true"); } else { contentAsNode.put("service", "false"); @@ -162,7 +165,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", hash(CharBuffer.wrap(plainTextPassword.toCharArray()), settings)); } else if (origHash != null && origHash.length() > 0) { contentAsNode.remove("password"); } else if (plainTextPassword != null && plainTextPassword.isEmpty() && origHash == null) { @@ -275,7 +278,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", hash(CharBuffer.wrap(plainTextPassword.toCharArray()), settings)); contentAsNode.put("enabled", "true"); contentAsNode.put("service", "true"); 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/AccountApiActionConfigValidationsTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiActionConfigValidationsTest.java index 8af780e01a..c2fdb436cb 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 @@ -19,6 +19,8 @@ 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 +37,7 @@ public void verifyValidCurrentPassword() { assertFalse(result.isValid()); assertEquals(RestStatus.BAD_REQUEST, result.status()); - u.setHash(Utils.hash("aaaa".toCharArray())); + u.setHash(Utils.hash(CharBuffer.wrap("aaaa".toCharArray()), securityApiDependencies.settings())); result = accountApiAction.validCurrentPassword(SecurityConfiguration.of(requestContent(), "u", configuration)); assertTrue(result.isValid()); } @@ -59,7 +61,7 @@ public void updatePassword() { assertTrue(OpenBSDBCrypt.checkPassword(u.getHash(), "cccccc".toCharArray())); requestContent.remove("password"); - requestContent.put("hash", Utils.hash("dddddd".toCharArray())); + requestContent.put("hash", Utils.hash(CharBuffer.wrap("dddddd".toCharArray()), securityApiDependencies.settings())); result = accountApiAction.updatePassword(SecurityConfiguration.of(requestContent, "u", configuration)); assertTrue(result.isValid()); assertTrue(OpenBSDBCrypt.checkPassword(u.getHash(), "dddddd".toCharArray())); @@ -71,7 +73,7 @@ private ObjectNode requestContent() { private InternalUserV7 createExistingUser() { final var u = new InternalUserV7(); - u.setHash(Utils.hash("sssss".toCharArray())); + u.setHash(Utils.hash(CharBuffer.wrap("sssss".toCharArray()), securityApiDependencies.settings())); 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..84438b8659 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,16 +12,17 @@ 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; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.dlic.rest.support.Utils; import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -98,7 +99,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(Utils.checkPassword(CharBuffer.wrap("aaaaaa".toCharArray()), securityConfiguration.requestContent().get("hash").asText(), securityApiDependencies.settings())); } @Test