Change password hashing defaults according to OWASP recommendations (#16629)
Changes according to the latest [OWASP cheat sheet for secure Password Storage](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2): - Changed default password hashing algorithm from pbkdf2-sha256 to pbkdf2-sha512 - Increased number of hash iterations for pbkdf2-sha1 from 20.000 to 1.300.000 - Increased number of hash iterations for pbkdf2-sha256 from 27.500 to 600.000 - Increased number of hash iterations for pbkdf2-sha512 from 30.000 to 210.000 - Adapt PasswordHashingTest to new defaults - The test testBenchmarkPasswordHashingConfigurations can be used to compare the different hashing configurations. - Document changes in changes document with note on performance and how to keep the old behaviour. - Log a warning at the first time when Pbkdf2PasswordHashProviderFactory is used directly Fixes #16629 Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
This commit is contained in:
parent
208e3a6529
commit
e7363905fa
9 changed files with 173 additions and 18 deletions
|
@ -1,4 +1,4 @@
|
|||
|
||||
=== Password database compromised
|
||||
|
||||
{project_name} does not store passwords in raw text but as hashed text, using the PBKDF2 hashing algorithm. {project_name} performs 27,500 hashing iterations, the number of iterations recommended by the security community. This number of hashing iterations can adversely affect performance as PBKDF2 hashing uses a significant amount of CPU resources.
|
||||
{project_name} does not store passwords in raw text but as hashed text, using the `PBKDF2-HMAC-SHA512` message digest algorithm. {project_name} performs `210,000` hashing iterations, the number of iterations recommended by the security community. This number of hashing iterations can adversely affect performance as PBKDF2 hashing uses a significant amount of CPU resources.
|
|
@ -225,6 +225,51 @@ PUT /admin/realms/{realm}/users/{id}/execute-actions-email
|
|||
|
||||
The compatibility mode for SAML encryption introduced in version 21 is now removed. The system property `keycloak.saml.deprecated.encryption` is not managed anymore by the server. The clients which still used the old signing key for encryption should update it from the new IDP configuration metadata.
|
||||
|
||||
= Changes to Password Hashing
|
||||
|
||||
In this release we adapted the password hashing defaults to match the https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2[OWASP recommendations for Password Storage].
|
||||
|
||||
As part of this change, the default password hashing provider has changed from `pbkdf2-sha256` to `pbkdf2-sha512`.
|
||||
Also, the number of default hash iterations for `pbkdf2` based password hashing algorithms changed as follows:
|
||||
[%autowidth,cols="a,a,>a,>a"]
|
||||
|===
|
||||
| Provider ID | Algorithm | Old Iterations | New Iterations
|
||||
|
||||
| `pbkdf2` | `PBKDF2WithHmacSHA1` | 20.000 | 1.300.000
|
||||
| `pbkdf2-sha256` | `PBKDF2WithHmacSHA256` | 27.500 | 600.000
|
||||
| `pbkdf2-sha512` | `PBKDF2WithHmacSHA512` | 30.000 | 210.000
|
||||
|===
|
||||
|
||||
If a realm does not explicitly configure a password policy with `hashAlgorithm` and `hashIterations`, then
|
||||
the new configuration will take effect on the next password based login, or when a user password is created or updated.
|
||||
|
||||
Note that the increased iteration counts can have a significant impact on the required CPU resources.
|
||||
|
||||
== Performance of new password hashing configuration
|
||||
|
||||
Tests on a machine with an Intel i9-8950HK CPU (12) @ 4.800GHz yielded the following ⌀ time differences for hashing 1000 passwords (averages from 3 runs).
|
||||
Note that the average duration for the `PBKDF2WithHmacSHA1` was computed with a lower number of passwords due to the long runtime.
|
||||
[%autowidth,cols="a,a,>a,>a,>a"]
|
||||
|===
|
||||
| Provider ID | Algorithm | Old duration | New duration | Difference
|
||||
|
||||
| `pbkdf2` | `PBKDF2WithHmacSHA1` | 122ms | 3.114ms | +2.992ms
|
||||
| `pbkdf2-sha256` | `PBKDF2WithHmacSHA256` | 20ms | 451ms | +431ms
|
||||
| `pbkdf2-sha512` | `PBKDF2WithHmacSHA512` | 33ms | 224ms | +191ms
|
||||
|===
|
||||
|
||||
Users of the `pbkdf2` provider might need to explicitly reduce the
|
||||
number of hash iterations to regain acceptable performance.
|
||||
This can be done by configuring the hash iterations explicitly in the password policy of the realm.
|
||||
|
||||
== How to keep using the old pbkdf2-sha256 password hashing?
|
||||
|
||||
To keep the old password hashing for a realm, specify `hashAlgorithm` and `hashIterations` explicitly in the
|
||||
realm password policy.
|
||||
|
||||
* `Hashing Algorithm: pbkdf2-sha256`
|
||||
* `Hashing Iterations: 27500`
|
||||
|
||||
= Renaming JPA provider configuration options for migration
|
||||
|
||||
After removal of the Map Store the following configuration options were renamed:
|
||||
|
|
|
@ -33,6 +33,8 @@ import java.security.spec.InvalidKeySpecException;
|
|||
import java.security.spec.KeySpec;
|
||||
|
||||
/**
|
||||
* Implementation PBKDF2 password hash algorithm.
|
||||
*
|
||||
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
||||
*/
|
||||
public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
||||
|
@ -137,4 +139,8 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
|||
throw new RuntimeException("PBKDF2 algorithm not found", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getPbkdf2Algorithm() {
|
||||
return pbkdf2Algorithm;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,21 +17,37 @@
|
|||
|
||||
package org.keycloak.credential.hash;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* Provider factory for SHA1 variant of the PBKDF2 password hash algorithm.
|
||||
*
|
||||
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
||||
* @deprecated The PBKDF2 provider with SHA1 and the recommended number of 1.300.000 iterations is known to be very slow. We recommend to use the PBKDF2 variants with SHA256 or SHA512 instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public class Pbkdf2PasswordHashProviderFactory extends AbstractPbkdf2PasswordHashProviderFactory implements PasswordHashProviderFactory {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(Pbkdf2PasswordHashProviderFactory.class);
|
||||
|
||||
public static final String ID = "pbkdf2";
|
||||
|
||||
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
|
||||
|
||||
public static final int DEFAULT_ITERATIONS = 20000;
|
||||
/**
|
||||
* Hash iterations for PBKDF2-HMAC-SHA1 according to the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2">Password Storage Cheat Sheet</a>.
|
||||
*/
|
||||
public static final int DEFAULT_ITERATIONS = 1_300_000;
|
||||
|
||||
private static boolean usageWarningPrinted;
|
||||
|
||||
@Override
|
||||
public PasswordHashProvider create(KeycloakSession session) {
|
||||
if (!usageWarningPrinted) {
|
||||
LOG.warnf("Detected usage of password hashing provider '%s'. The provider is no longer recommended, use 'pbkdf2-sha256' or 'pbkdf2-sha512' instead.", ID);
|
||||
usageWarningPrinted = true;
|
||||
}
|
||||
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS, getMaxPaddingLength());
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,10 @@ public class Pbkdf2Sha256PasswordHashProviderFactory extends AbstractPbkdf2Passw
|
|||
|
||||
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256";
|
||||
|
||||
public static final int DEFAULT_ITERATIONS = 27500;
|
||||
/**
|
||||
* Hash iterations for PBKDF2-HMAC-SHA256 according to the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2">Password Storage Cheat Sheet</a>.
|
||||
*/
|
||||
public static final int DEFAULT_ITERATIONS = 600_000;
|
||||
|
||||
@Override
|
||||
public PasswordHashProvider create(KeycloakSession session) {
|
||||
|
|
|
@ -13,7 +13,10 @@ public class Pbkdf2Sha512PasswordHashProviderFactory extends AbstractPbkdf2Passw
|
|||
|
||||
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA512";
|
||||
|
||||
public static final int DEFAULT_ITERATIONS = 30000;
|
||||
/**
|
||||
* Hash iterations for PBKDF2-HMAC-SHA512 according to the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2">Password Storage Cheat Sheet</a>.
|
||||
*/
|
||||
public static final int DEFAULT_ITERATIONS = 210_000;
|
||||
|
||||
@Override
|
||||
public PasswordHashProvider create(KeycloakSession session) {
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package org.keycloak.models;
|
||||
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.policy.PasswordPolicyConfigException;
|
||||
import org.keycloak.policy.PasswordPolicyProvider;
|
||||
|
||||
|
@ -34,11 +33,11 @@ public class PasswordPolicy implements Serializable {
|
|||
|
||||
public static final String HASH_ALGORITHM_ID = "hashAlgorithm";
|
||||
|
||||
public static final String HASH_ALGORITHM_DEFAULT = "pbkdf2-sha256";
|
||||
public static final String HASH_ALGORITHM_DEFAULT = "pbkdf2-sha512";
|
||||
|
||||
public static final String HASH_ITERATIONS_ID = "hashIterations";
|
||||
|
||||
public static final int HASH_ITERATIONS_DEFAULT = 27500;
|
||||
public static final int HASH_ITERATIONS_DEFAULT = 210_000;
|
||||
|
||||
public static final String PASSWORD_HISTORY_ID = "passwordHistory";
|
||||
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.forms;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.credential.hash.PasswordHashProvider;
|
||||
import org.keycloak.credential.hash.PasswordHashProviderFactory;
|
||||
import org.keycloak.credential.hash.Pbkdf2PasswordHashProvider;
|
||||
import org.keycloak.credential.hash.Pbkdf2PasswordHashProviderFactory;
|
||||
import org.keycloak.credential.hash.Pbkdf2Sha256PasswordHashProviderFactory;
|
||||
|
@ -40,8 +42,14 @@ import org.keycloak.testsuite.util.UserBuilder;
|
|||
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -114,7 +122,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username));
|
||||
|
||||
assertEquals(1, credential.getPasswordCredentialData().getHashIterations());
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", 1);
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", 1);
|
||||
}
|
||||
|
||||
// KEYCLOAK-5282
|
||||
|
@ -139,7 +147,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
assertEquals(credentialId, credential.getId());
|
||||
assertArrayEquals(salt, credential.getPasswordSecretData().getSalt());
|
||||
|
||||
setPasswordPolicy("hashIterations(" + Pbkdf2Sha256PasswordHashProviderFactory.DEFAULT_ITERATIONS + ")");
|
||||
setPasswordPolicy("hashIterations(" + Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS + ")");
|
||||
|
||||
AccountHelper.logout(adminClient.realm("test"), username);
|
||||
|
||||
|
@ -187,7 +195,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
createUser(username);
|
||||
|
||||
PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username));
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA1", 20000);
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA1", Pbkdf2PasswordHashProviderFactory.DEFAULT_ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -197,7 +205,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
createUser(username);
|
||||
|
||||
PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username));
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", 27500);
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -207,7 +215,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
createUser(username);
|
||||
|
||||
PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username));
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", 27500);
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", Pbkdf2Sha256PasswordHashProviderFactory.DEFAULT_ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -217,7 +225,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
createUser(username);
|
||||
|
||||
PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username));
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", 30000);
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -231,7 +239,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
createUser(username1);
|
||||
|
||||
PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username1));
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", 27500);
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", Pbkdf2Sha256PasswordHashProviderFactory.DEFAULT_ITERATIONS);
|
||||
|
||||
// Now configure padding to bigger than 64. The verification without padding would fail as for longer padding than 64 characters, the hashes of the padded password and unpadded password would be different
|
||||
configurePaddingForKeycloak(65);
|
||||
|
@ -239,7 +247,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
createUser(username2);
|
||||
|
||||
credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username2));
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", 27500, false);
|
||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", Pbkdf2Sha256PasswordHashProviderFactory.DEFAULT_ITERATIONS, false);
|
||||
|
||||
} finally {
|
||||
configurePaddingForKeycloak(originalPaddingLength);
|
||||
|
@ -295,4 +303,69 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
}, Integer.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple test to compare runtimes of different password hashing configurations.
|
||||
*/
|
||||
// @Test
|
||||
public void testBenchmarkPasswordHashingConfigurations() {
|
||||
|
||||
int numberOfPasswords = 1000;
|
||||
List<String> plainTextPasswords = IntStream.rangeClosed(1, numberOfPasswords).mapToObj(i -> UUID.randomUUID().toString()).collect(Collectors.toList());
|
||||
|
||||
Function<Runnable, Duration> timeit = runner -> {
|
||||
|
||||
long time = -System.nanoTime();
|
||||
runner.run();
|
||||
time += System.nanoTime();
|
||||
|
||||
return Duration.ofNanos((long) (time / ((double) plainTextPasswords.size())));
|
||||
};
|
||||
|
||||
BiFunction<PasswordHashProvider, Integer, Long> hasher = (provider, iterations) -> {
|
||||
long result = 0L;
|
||||
for (String password : plainTextPasswords) {
|
||||
String encoded = provider.encode(password, iterations);
|
||||
result += encoded.hashCode();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
var comparisons = List.of(
|
||||
// this takes quite a long time. Run this with a low value numberOfPasswords, e.g. 1-10
|
||||
// new PasswordHashComparison(new Pbkdf2PasswordHashProviderFactory(), 20_000, 1_300_000),
|
||||
|
||||
new PasswordHashComparison(new Pbkdf2Sha256PasswordHashProviderFactory(), 27_500, 600_000),
|
||||
new PasswordHashComparison(new Pbkdf2Sha512PasswordHashProviderFactory(), 30_000, 210_000)
|
||||
);
|
||||
|
||||
comparisons.forEach(comp -> {
|
||||
Pbkdf2PasswordHashProvider hashProvider = (Pbkdf2PasswordHashProvider) comp.factory.create(null);
|
||||
System.out.printf("Hashing %s password(s) with %s%n", plainTextPasswords.size(), hashProvider.getPbkdf2Algorithm());
|
||||
|
||||
var durationOld = timeit.apply(() -> hasher.apply(hashProvider, comp.iterationsOld));
|
||||
System.out.printf("\tØ hashing duration with %d iterations: %sms%n", comp.iterationsOld, durationOld.toMillis());
|
||||
|
||||
var durationNew = timeit.apply(() -> hasher.apply(hashProvider, comp.iterationsNew));
|
||||
System.out.printf("\tØ hashing duration with %d iterations: %sms%n", comp.iterationsNew, durationNew.toMillis());
|
||||
|
||||
var deltaTimeMillis = durationNew.toMillis() - durationOld.toMillis();
|
||||
System.out.printf("\tDifference: +%s ms%n", deltaTimeMillis);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
class PasswordHashComparison {
|
||||
|
||||
final PasswordHashProviderFactory factory;
|
||||
|
||||
final int iterationsOld;
|
||||
|
||||
final int iterationsNew;
|
||||
|
||||
public PasswordHashComparison(PasswordHashProviderFactory factory, int iterationsOld, int iterationsNew) {
|
||||
this.factory = factory;
|
||||
this.iterationsOld = iterationsOld;
|
||||
this.iterationsNew = iterationsNew;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.testsuite.policy;
|
|||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.credential.hash.Pbkdf2Sha512PasswordHashProviderFactory;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -49,6 +50,15 @@ import static org.junit.Assert.fail;
|
|||
*/
|
||||
public class PasswordPolicyTest extends AbstractKeycloakTest {
|
||||
|
||||
@Test
|
||||
public void testDefaultPasswordPolicySettings() {
|
||||
testingClient.server("passwordPolicy").run(session -> {
|
||||
RealmModel realmModel = session.getContext().getRealm();
|
||||
PasswordPolicy passwordPolicy = realmModel.getPasswordPolicy();
|
||||
Assert.assertEquals(Pbkdf2Sha512PasswordHashProviderFactory.ID, passwordPolicy.getHashAlgorithm());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLength() {
|
||||
testingClient.server("passwordPolicy").run(session -> {
|
||||
|
|
Loading…
Reference in a new issue