Skip to content

Commit

Permalink
Add support for PBKDF2, Scrypt, and Argon2 for password hashing (open…
Browse files Browse the repository at this point in the history
…search-project#4079)

Signed-off-by: Dan Cecoi <[email protected]>
  • Loading branch information
Dan Cecoi authored and Dan Cecoi committed May 14, 2024
1 parent 3b94522 commit a51584d
Show file tree
Hide file tree
Showing 32 changed files with 1,741 additions and 92 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

This comment has been minimized.

Copy link
@terryquigleysas

terryquigleysas May 14, 2024

Collaborator

I think we should use the most recent version before we submit to them.


//JWT
implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}"
implementation "io.jsonwebtoken:jjwt-impl:${jjwt_version}"
Expand Down
3 changes: 3 additions & 0 deletions plugin-security.policy
Original file line number Diff line number Diff line change
Expand Up @@ -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";

This comment has been minimized.

Copy link
@terryquigleysas

terryquigleysas May 14, 2024

Collaborator

Can we remove this once we use the most recent version of password4j?


};

grant codeBase "${codebase.netty-common}" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit a51584d

Please sign in to comment.