Use Argon2 as default password hashing algorithm (#28162)

Closes #28161

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-03-22 14:04:14 +01:00 committed by GitHub
parent 31293d36e8
commit c3a98ae387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 120 additions and 42 deletions

View file

@ -1,6 +1,7 @@
package org.keycloak.crypto.hash; package org.keycloak.crypto.hash;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator; import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.credential.hash.PasswordHashProvider; import org.keycloak.credential.hash.PasswordHashProvider;
@ -21,6 +22,8 @@ import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.TYPE_KE
import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.VERSION_KEY; import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.VERSION_KEY;
public class Argon2PasswordHashProvider implements PasswordHashProvider { public class Argon2PasswordHashProvider implements PasswordHashProvider {
private static final Logger logger = Logger.getLogger(Argon2PasswordHashProvider.class);
private final String version; private final String version;
private final String type; private final String type;
private final int hashLength; private final int hashLength;
@ -56,7 +59,14 @@ public class Argon2PasswordHashProvider implements PasswordHashProvider {
* policy. * policy.
*/ */
@Override @Override
public PasswordCredentialModel encodedCredential(String rawPassword, int ignoredIterationsFromPasswordPolicy) { public PasswordCredentialModel encodedCredential(String rawPassword, int iterations) {
if (iterations == -1) {
iterations = this.iterations;
} else if (iterations > 100) {
logger.warn("Iterations for Argon should be less than 100, using default");
iterations = this.iterations;
}
byte[] salt = Salt.generateSalt(); byte[] salt = Salt.generateSalt();
String encoded = encode(rawPassword, salt, version, type, hashLength, parallelism, memory, iterations); String encoded = encode(rawPassword, salt, version, type, hashLength, parallelism, memory, iterations);

View file

@ -110,8 +110,12 @@ public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFa
} }
@Override @Override
public boolean isSupported(Config.Scope config) { public boolean isSupported() {
return !Profile.isFeatureEnabled(Profile.Feature.FIPS); return !Profile.isFeatureEnabled(Profile.Feature.FIPS);
} }
@Override
public int order() {
return 300;
}
} }

View file

@ -1,4 +1,6 @@
= Argon2 password hashing provider = Argon2 password hashing
Argon2 is now the default password hashing algorithm used by {project_name}
Argon2 was the winner of the [2015 password hashing competition](https://en.wikipedia.org/wiki/Password_Hashing_Competition) Argon2 was the winner of the [2015 password hashing competition](https://en.wikipedia.org/wiki/Password_Hashing_Competition)
and is the recommended hashing algorithm by [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id). and is the recommended hashing algorithm by [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id).
@ -6,7 +8,7 @@ and is the recommended hashing algorithm by [OWASP](https://cheatsheetseries.owa
In {project_name} 24 the default hashing iterations for PBKDF2 were increased from 27.5K to 210K, resulting in a more than In {project_name} 24 the default hashing iterations for PBKDF2 were increased from 27.5K to 210K, resulting in a more than
10 times increase in the amount of CPU time required to generate a password hash. With Argon2 it is possible to achieve 10 times increase in the amount of CPU time required to generate a password hash. With Argon2 it is possible to achieve
better security, with almost the same CPU time as previous releases of {project_name}. One downside is Argon2 requires more better security, with almost the same CPU time as previous releases of {project_name}. One downside is Argon2 requires more
memory, which is a requirement to be resistant against GPU attacks. The defaults for Argon2 in Keycloak requires 7MB memory, which is a requirement to be resistant against GPU attacks. The defaults for Argon2 in {project_name} requires 7MB
per-hashing request. per-hashing request.
= Deprecated cookie methods removed = Deprecated cookie methods removed
@ -19,4 +21,4 @@ The following methods for setting custom cookies have been removed:
= Searching by user attribute no longer case insensitive = Searching by user attribute no longer case insensitive
When searching for users by user attribute, Keycloak no longer searches for user attribute names forcing lower case comparisons. The goal of this change was to speed up searches by using Keycloak's native index on the user attribute table. If your database collation is case-insensitive, your search results will stay the same. If your database collation is case-sensitive, you might see less search results than before. When searching for users by user attribute, {project_name} no longer searches for user attribute names forcing lower case comparisons. The goal of this change was to speed up searches by using {project_name}'s native index on the user attribute table. If your database collation is case-insensitive, your search results will stay the same. If your database collation is case-sensitive, you might see less search results than before.

View file

@ -30,11 +30,16 @@ Passwords are not stored in cleartext. Before storage or validation, {project_na
Supported password hashing algorithms include: Supported password hashing algorithms include:
* argon2:: Argon2 (recommended for non-FIPS deployments) * argon2:: Argon2 (default for non-FIPS deployments)
* pbkdf2-sha512:: PBKDF2 with SHA512 (default, recommended for FIPS deployments) * pbkdf2-sha512:: PBKDF2 with SHA512 (default for FIPS deployments)
* pbkdf2-sha256:: PBKDF2 with SHA256 * pbkdf2-sha256:: PBKDF2 with SHA256
* pbkdf2:: PBKDF2 with SHA1 (deprecated) * pbkdf2:: PBKDF2 with SHA1 (deprecated)
It is highly recommended to use Argon2 when possible as it has significantly less CPU requirements compared to PBKDF2, while
at the same time being more secure.
The default password hashing algorithm for the server can be configured with `--spi-password-hashing-provider-default=<algorithm>`.
See the link:{developerguide_link}[{developerguide_name}] on how to add your own hashing algorithm. See the link:{developerguide_link}[{developerguide_name}] on how to add your own hashing algorithm.
[NOTE] [NOTE]
@ -43,17 +48,18 @@ If you change the hashing algorithm, password hashes in storage will not change
==== ====
===== Hashing iterations ===== Hashing iterations
Specifies the number of times {project_name} hashes passwords before storage or verification. The default value is 210,000 in case that `pbkdf2-sha512` is used as hashing algorithm, which is by default. Specifies the number of times {project_name} hashes passwords before storage or verification. The default value is -1,
If other hash algorithms are explicitly set by using the `HashAlgorithm` policy, the default count of hashing iterations could be different. For instance, it is 600,000 by default if the `pbkdf2-sha256` algorithm is used or 1,300,000 if which uses the default hashing intervals for the selected hashing algorithm:
the `pbkdf2` algorithm (Algorithm `pbkdf2` corresponds to PBKDF2 with HMAC-SHA1).
When using Argon2 as the hashing algorithm the hashing iterations for the password policy is ignored. * argon2:: 5
* pbkdf2-sha512:: 210,000
{project_name} hashes passwords to ensure that hostile actors with access to the password database cannot read passwords through reverse engineering. * pbkdf2-sha256:: 600,000
* pbkdf2:: 1,300,000
[NOTE] [NOTE]
==== ====
A high hashing iteration value can impact performance as it requires higher CPU power. In most cases the hashing iterations should not be changed from the recommended default values. Lower values for
iterations provide insufficient security, while higher values result in higher CPU power requirements.
==== ====
===== Digits ===== Digits

View file

@ -151,7 +151,7 @@ public class BackwardsCompatibilityUserStorage implements UserLookupProvider, Us
hashProvider.encode(userCredentialModel.getValue(), policy.getHashIterations(), newPassword); hashProvider.encode(userCredentialModel.getValue(), policy.getHashIterations(), newPassword);
// Test expected values of credentialModel // Test expected values of credentialModel
assertEquals(newPassword.getAlgorithm(), Pbkdf2Sha512PasswordHashProviderFactory.ID); assertNotNull(newPassword.getAlgorithm());
assertNotNull(newPassword.getValue()); assertNotNull(newPassword.getValue());
assertNotNull(newPassword.getSalt()); assertNotNull(newPassword.getSalt());

View file

@ -87,6 +87,7 @@ import org.keycloak.testsuite.util.AccountHelper;
import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.DefaultPasswordHash;
import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.GroupBuilder; import org.keycloak.testsuite.util.GroupBuilder;
import org.keycloak.testsuite.util.MailUtils; import org.keycloak.testsuite.util.MailUtils;
@ -524,8 +525,8 @@ public class UserTest extends AbstractAdminTest {
CredentialModel credential = fetchCredentials("user_rawpw"); CredentialModel credential = fetchCredentials("user_rawpw");
assertNotNull("Expecting credential", credential); assertNotNull("Expecting credential", credential);
PasswordCredentialModel pcm = PasswordCredentialModel.createFromCredentialModel(credential); PasswordCredentialModel pcm = PasswordCredentialModel.createFromCredentialModel(credential);
assertEquals(Pbkdf2Sha512PasswordHashProviderFactory.ID, pcm.getPasswordCredentialData().getAlgorithm()); assertEquals(DefaultPasswordHash.getDefaultAlgorithm(), pcm.getPasswordCredentialData().getAlgorithm());
assertEquals(Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS, pcm.getPasswordCredentialData().getHashIterations()); assertEquals(DefaultPasswordHash.getDefaultIterations(), pcm.getPasswordCredentialData().getHashIterations());
assertNotEquals("ABCD", pcm.getPasswordSecretData().getValue()); assertNotEquals("ABCD", pcm.getPasswordSecretData().getValue());
assertEquals(CredentialRepresentation.PASSWORD, credential.getType()); assertEquals(CredentialRepresentation.PASSWORD, credential.getType());
} }
@ -2774,8 +2775,8 @@ public class UserTest extends AbstractAdminTest {
PasswordCredentialModel credential = PasswordCredentialModel PasswordCredentialModel credential = PasswordCredentialModel
.createFromCredentialModel(fetchCredentials("user_rawpw")); .createFromCredentialModel(fetchCredentials("user_rawpw"));
assertNotNull("Expecting credential", credential); assertNotNull("Expecting credential", credential);
assertEquals(Pbkdf2Sha512PasswordHashProviderFactory.ID, credential.getPasswordCredentialData().getAlgorithm()); assertEquals(DefaultPasswordHash.getDefaultAlgorithm(), credential.getPasswordCredentialData().getAlgorithm());
assertEquals(Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS, credential.getPasswordCredentialData().getHashIterations()); assertEquals(DefaultPasswordHash.getDefaultIterations(), credential.getPasswordCredentialData().getHashIterations());
assertNotEquals("ABCD", credential.getPasswordSecretData().getValue()); assertNotEquals("ABCD", credential.getPasswordSecretData().getValue());
assertEquals(CredentialRepresentation.PASSWORD, credential.getType()); assertEquals(CredentialRepresentation.PASSWORD, credential.getType());
@ -2792,8 +2793,8 @@ public class UserTest extends AbstractAdminTest {
PasswordCredentialModel updatedCredential = PasswordCredentialModel PasswordCredentialModel updatedCredential = PasswordCredentialModel
.createFromCredentialModel(fetchCredentials("user_rawpw")); .createFromCredentialModel(fetchCredentials("user_rawpw"));
assertNotNull("Expecting credential", updatedCredential); assertNotNull("Expecting credential", updatedCredential);
assertEquals(Pbkdf2Sha512PasswordHashProviderFactory.ID, updatedCredential.getPasswordCredentialData().getAlgorithm()); assertEquals(DefaultPasswordHash.getDefaultAlgorithm(), updatedCredential.getPasswordCredentialData().getAlgorithm());
assertEquals(Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS, updatedCredential.getPasswordCredentialData().getHashIterations()); assertEquals(DefaultPasswordHash.getDefaultIterations(), updatedCredential.getPasswordCredentialData().getHashIterations());
assertNotEquals("EFGH", updatedCredential.getPasswordSecretData().getValue()); assertNotEquals("EFGH", updatedCredential.getPasswordSecretData().getValue());
assertEquals(CredentialRepresentation.PASSWORD, updatedCredential.getType()); assertEquals(CredentialRepresentation.PASSWORD, updatedCredential.getType());
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.forms; package org.keycloak.testsuite.forms;
import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.BadRequestException;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Assume; import org.junit.Assume;
@ -30,6 +31,8 @@ import org.keycloak.credential.hash.Pbkdf2PasswordHashProvider;
import org.keycloak.credential.hash.Pbkdf2PasswordHashProviderFactory; import org.keycloak.credential.hash.Pbkdf2PasswordHashProviderFactory;
import org.keycloak.credential.hash.Pbkdf2Sha256PasswordHashProviderFactory; import org.keycloak.credential.hash.Pbkdf2Sha256PasswordHashProviderFactory;
import org.keycloak.credential.hash.Pbkdf2Sha512PasswordHashProviderFactory; import org.keycloak.credential.hash.Pbkdf2Sha512PasswordHashProviderFactory;
import org.keycloak.crypto.hash.Argon2Parameters;
import org.keycloak.crypto.hash.Argon2PasswordHashProvider;
import org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory; import org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -45,6 +48,7 @@ import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.AccountHelper; import org.keycloak.testsuite.util.AccountHelper;
import org.keycloak.testsuite.util.DefaultPasswordHash;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import javax.crypto.SecretKeyFactory; import javax.crypto.SecretKeyFactory;
@ -79,7 +83,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
protected AppPage appPage; protected AppPage appPage;
@Test @Test
public void testSetInvalidProvider() throws Exception { public void testSetInvalidProvider() {
try { try {
setPasswordPolicy("hashAlgorithm(nosuch)"); setPasswordPolicy("hashAlgorithm(nosuch)");
fail("Expected error"); fail("Expected error");
@ -131,29 +135,34 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username)); credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username));
assertEquals(Pbkdf2Sha512PasswordHashProviderFactory.ID, credential.getPasswordCredentialData().getAlgorithm()); assertEquals(DefaultPasswordHash.getDefaultAlgorithm(), credential.getPasswordCredentialData().getAlgorithm());
} }
@Test @Test
public void testPasswordRehashedOnIterationsChanged() throws Exception { public void testPasswordRehashedOnIterationsChanged() throws Exception {
setPasswordPolicy("hashIterations(10000)"); setPasswordPolicy("hashIterations(1)");
String username = "testPasswordRehashedOnIterationsChanged"; String username = "testPasswordRehashedOnIterationsChanged";
createUser(username); createUser(username);
PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username)); PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username));
assertEquals(10000, credential.getPasswordCredentialData().getHashIterations()); assertEquals(1, credential.getPasswordCredentialData().getHashIterations());
setPasswordPolicy("hashIterations(1)"); setPasswordPolicy("hashIterations(2)");
loginPage.open(); loginPage.open();
loginPage.login(username, "password"); loginPage.login(username, "password");
credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username)); credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username));
assertEquals(1, credential.getPasswordCredentialData().getHashIterations()); assertEquals(2, credential.getPasswordCredentialData().getHashIterations());
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", 1);
if (notFips()) {
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "Argon2id", 2);
} else {
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", 2);
}
} }
// KEYCLOAK-5282 // KEYCLOAK-5282
@ -218,7 +227,6 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
} }
@Test @Test
public void testPbkdf2Sha1() throws Exception { public void testPbkdf2Sha1() throws Exception {
setPasswordPolicy("hashAlgorithm(" + Pbkdf2PasswordHashProviderFactory.ID + ")"); setPasswordPolicy("hashAlgorithm(" + Pbkdf2PasswordHashProviderFactory.ID + ")");
@ -231,7 +239,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void testArgon2() { public void testArgon2() {
Assume.assumeTrue("Argon2 tests skipped in FIPS mode", AuthServerTestEnricher.AUTH_SERVER_FIPS_MODE == FipsMode.DISABLED); Assume.assumeTrue("Argon2 tests skipped in FIPS mode", notFips());
setPasswordPolicy("hashAlgorithm(" + Argon2PasswordHashProviderFactory.ID + ")"); setPasswordPolicy("hashAlgorithm(" + Argon2PasswordHashProviderFactory.ID + ")");
String username = "testArgon2"; String username = "testArgon2";
@ -258,14 +266,22 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
appPage.assertCurrent(); appPage.assertCurrent();
} }
private static boolean notFips() {
return AuthServerTestEnricher.AUTH_SERVER_FIPS_MODE == FipsMode.DISABLED;
}
@Test @Test
public void testDefault() throws Exception { public void testDefault() throws Exception {
setPasswordPolicy(""); setPasswordPolicy("");
String username = "testDefault"; String username = "testDefault";
createUser(username); createUser(username);
PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username)); PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username));
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS);
if (notFips()) {
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "Argon2id", Argon2Parameters.DEFAULT_ITERATIONS);
} else {
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS);
}
} }
@Test @Test
@ -339,18 +355,34 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
} }
private void assertEncoded(PasswordCredentialModel credential, String password, byte[] salt, String algorithm, int iterations, boolean expectedSuccess) throws Exception { private void assertEncoded(PasswordCredentialModel credential, String password, byte[] salt, String algorithm, int iterations, boolean expectedSuccess) throws Exception {
int keyLength = 512; if (algorithm.startsWith("PBKDF2")) {
int keyLength = 512;
if (Pbkdf2Sha256PasswordHashProviderFactory.ID.equals(credential.getPasswordCredentialData().getAlgorithm())) { if (Pbkdf2Sha256PasswordHashProviderFactory.ID.equals(credential.getPasswordCredentialData().getAlgorithm())) {
keyLength = 256; keyLength = 256;
} }
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, keyLength); KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, keyLength);
byte[] key = SecretKeyFactory.getInstance(algorithm).generateSecret(spec).getEncoded(); byte[] key = SecretKeyFactory.getInstance(algorithm).generateSecret(spec).getEncoded();
if (expectedSuccess) { if (expectedSuccess) {
assertEquals(Base64.encodeBytes(key), credential.getPasswordSecretData().getValue()); assertEquals(Base64.encodeBytes(key), credential.getPasswordSecretData().getValue());
} else { } else {
assertNotEquals(Base64.encodeBytes(key), credential.getPasswordSecretData().getValue()); assertNotEquals(Base64.encodeBytes(key), credential.getPasswordSecretData().getValue());
}
} else if (algorithm.equals("Argon2id")) {
org.bouncycastle.crypto.params.Argon2Parameters parameters = new org.bouncycastle.crypto.params.Argon2Parameters.Builder(org.bouncycastle.crypto.params.Argon2Parameters.ARGON2_id)
.withVersion(org.bouncycastle.crypto.params.Argon2Parameters.ARGON2_VERSION_13)
.withSalt(salt)
.withParallelism(1)
.withMemoryAsKB(7168)
.withIterations(iterations).build();
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(parameters);
byte[] result = new byte[32];
generator.generateBytes(password.toCharArray(), result);
Assert.assertEquals(Base64.encodeBytes(result), credential.getPasswordSecretData().getValue());
} }
} }

View file

@ -0,0 +1,23 @@
package org.keycloak.testsuite.util;
import org.keycloak.common.crypto.FipsMode;
import org.keycloak.credential.hash.Pbkdf2Sha512PasswordHashProviderFactory;
import org.keycloak.crypto.hash.Argon2Parameters;
import org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
public class DefaultPasswordHash {
public static String getDefaultAlgorithm() {
return notFips() ? Argon2PasswordHashProviderFactory.ID : Pbkdf2Sha512PasswordHashProviderFactory.ID;
}
public static int getDefaultIterations() {
return notFips() ? Argon2Parameters.DEFAULT_ITERATIONS : Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS;
}
private static boolean notFips() {
return AuthServerTestEnricher.AUTH_SERVER_FIPS_MODE == FipsMode.DISABLED;
}
}