Skip to content

Adds Argon2 support for password hashing #5441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5568e37
first commit on clean branch of old feature changes
aidenlindsay Jun 25, 2025
5e927e8
screwed up import statement removed
aidenlindsay Jun 25, 2025
b04dacf
fix: fixed the hash redaction by changing the regex in AuditMessage
aidenlindsay Jun 25, 2025
b37dcdd
SpotlessApply to clean up
aidenlindsay Jun 25, 2025
8a45748
removed System.out.println statement
aidenlindsay Jun 25, 2025
f0c0085
Made changes to changelog
aidenlindsay Jun 25, 2025
6a05af5
another spotless apply
aidenlindsay Jun 25, 2025
39ccb17
tidy: used the @Parameterized pattern to clean up PasswordHasherTests…
aidenlindsay Jun 26, 2025
c632e10
tidy: remove unnecessary legacy JSM code in Argon2, will see if the s…
aidenlindsay Jun 26, 2025
24fef56
tidy: made similar removal changes of legacy JSM code from the other …
aidenlindsay Jun 26, 2025
7fda6bb
fix: use reference to the actual default values from the config file …
aidenlindsay Jun 26, 2025
e6cf061
tidy: changed changelog into present tense
aidenlindsay Jun 26, 2025
49fe116
test: added tests for passing an invalid hash, and ensuring that fals…
aidenlindsay Jun 26, 2025
25226c2
fix: changes with config Constants so that they are parsed to the cor…
aidenlindsay Jun 26, 2025
ca5a5b0
spent a while trying to find the cause of the unauth error in my test…
aidenlindsay Jun 27, 2025
12bdbf4
bulk integration tests all passed bar the cluster test, trying to get…
aidenlindsay Jun 27, 2025
ca937c9
Merge branch 'main' into argon2-feature-to-main
DarshitChanpura Jun 30, 2025
07515fe
Merge branch 'main' into argon2-feature-to-main
aidenlindsay Jun 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

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

### Enhancements

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

### Documentation


[Unreleased 3.x]: https://github.com/opensearch-project/security/compare/3.1...main
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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.util.List;
import java.util.Map;

import org.apache.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.BeforeClass;
import org.junit.Test;

import org.opensearch.security.support.ConfigConstants;
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 static org.hamcrest.Matchers.equalTo;
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 final String PASSWORD = "top$ecret1234!";

private static String type;
private static int memory, iterations, parallelism, length, version;

@BeforeClass
public static void startCluster() {

type = randomFrom(List.of("argon2id", "argon2i", "argon2d"));
iterations = randomFrom(List.of(2, 3, 4));
memory = randomFrom(List.of(65536, 131072));
parallelism = randomFrom(List.of(1, 2));
length = randomFrom(List.of(16, 32, 64));
version = randomFrom(List.of(16, 19));

TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
.hash(generateArgon2Hash("secret", memory, iterations, parallelism, length, type, version));
cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
.authc(AUTHC_HTTPBASIC_INTERNAL)
.users(ADMIN_USER)
.anonymousAuth(false)
.nodeSettings(
Map.of(
ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED,
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM,
ConfigConstants.ARGON2,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY,
memory,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS,
iterations,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM,
parallelism,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH,
length,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE,
type,
ConfigConstants.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, memory, iterations, parallelism, length, type, version);
createUserWithHashedPassword(cluster, "user_1", hash);
testPasswordAuth(cluster, "user_1", PASSWORD, HttpStatus.SC_OK);

createUserWithPlainTextPassword(cluster, "user_2", PASSWORD);
testPasswordAuth(cluster, "user_2", PASSWORD, HttpStatus.SC_OK);
}

@Test
public void shouldNotAuthenticateWithIncorrectPassword() {
String hash = generateArgon2Hash(PASSWORD, memory, iterations, parallelism, length, type, version);
createUserWithHashedPassword(cluster, "user_3", hash);
testPasswordAuth(cluster, "user_3", "wrong_password", HttpStatus.SC_UNAUTHORIZED);

createUserWithPlainTextPassword(cluster, "user_4", PASSWORD);
testPasswordAuth(cluster, "user_4", "wrong_password", HttpStatus.SC_UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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.util.List;
import java.util.Map;

import org.apache.http.HttpStatus;
import org.junit.ClassRule;
import org.junit.Test;

import org.opensearch.security.support.ConfigConstants;
import org.opensearch.test.framework.TestSecurityConfig;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;

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",
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT
)
);

@ClassRule
public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
.authc(AUTHC_HTTPBASIC_INTERNAL)
.users(ADMIN_USER)
.anonymousAuth(false)
.nodeSettings(
Map.of(
ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED,
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM,
ConfigConstants.ARGON2
)
)
.build();

@Test
public void shouldAuthenticateWithCorrectPassword() {
String hash = generateArgon2Hash(
PASSWORD,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT
);
createUserWithHashedPassword(cluster, "user_1", hash);
testPasswordAuth(cluster, "user_1", PASSWORD, HttpStatus.SC_OK);

createUserWithPlainTextPassword(cluster, "user_2", PASSWORD);
testPasswordAuth(cluster, "user_2", PASSWORD, HttpStatus.SC_OK);
}

@Test
public void shouldNotAuthenticateWithIncorrectPassword() {
String hash = generateArgon2Hash(
PASSWORD,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_TYPE_DEFAULT,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_VERSION_DEFAULT
);
createUserWithHashedPassword(cluster, "user_3", hash);
testPasswordAuth(cluster, "user_3", "wrongpassword", HttpStatus.SC_UNAUTHORIZED);

createUserWithPlainTextPassword(cluster, "user_4", PASSWORD);
testPasswordAuth(cluster, "user_4", "wrongpassword", HttpStatus.SC_UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;

import com.password4j.Argon2Function;
import com.password4j.BcryptFunction;
import com.password4j.CompressedPBKDF2Function;
import com.password4j.Password;
import com.password4j.types.Argon2;
import com.password4j.types.Bcrypt;

import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -78,4 +80,23 @@ public static String generatePBKDF2Hash(String password, String algorithm, int i
.getResult();
}

public static String generateArgon2Hash(
String password,
int memory,
int iterations,
int parallelism,
int length,
String type,
int version
) {
Argon2 argon2Type = switch (type.toUpperCase()) {
case "ARGON2ID" -> Argon2.ID;
case "ARGON2I" -> Argon2.I;
case "ARGON2D" -> Argon2.D;
default -> throw new IllegalArgumentException("Unknown Argon2 type: " + type);
};
return Password.hash(CharBuffer.wrap(password.toCharArray()))
.with(Argon2Function.getInstance(memory, iterations, parallelism, length, argon2Type, version))
.getResult();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,55 @@ public List<Setting<?>> getSettings() {
)
);

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_MEMORY,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_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.intSetting(
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH,
ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_LENGTH_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
)
);

if (!SSLConfig.isSslOnlyMode()) {
settings.add(
Setting.listSetting(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ public final class AuditMessage {
@VisibleForTesting
public static final String BCRYPT_REGEX = "\\$2[ayb]\\$.{56}";
public static final String PBKDF2_REGEX = "\\$\\d+\\$\\d+\\$[A-Za-z0-9+/]+={0,2}\\$[A-Za-z0-9+/]+={0,2}";
public static final Pattern HASH_REGEX_PATTERN = Pattern.compile(BCRYPT_REGEX + "|" + PBKDF2_REGEX);
public static final String ARGON2_REGEX =
"\\$argon2(?:id|i|d)\\$v=\\d+\\$(?:[a-z]=\\d+,?)+\\$[A-Za-z0-9+/]+={0,2}\\$[A-Za-z0-9+/]+={0,2}";
public static final Pattern HASH_REGEX_PATTERN = Pattern.compile(BCRYPT_REGEX + "|" + PBKDF2_REGEX + "|" + ARGON2_REGEX);
private static final String HASH_REPLACEMENT_VALUE = "__HASH__";
private static final String INTERNALUSERS_DOC_ID = CType.INTERNALUSERS.toLCString();

Expand Down
Loading
Loading