diff --git a/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProvider.java index 283d5361161..515605f8af2 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProvider.java @@ -15,6 +15,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Semaphore; import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.MEMORY_KEY; import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.PARALLELISM_KEY; @@ -30,14 +31,16 @@ public class Argon2PasswordHashProvider implements PasswordHashProvider { private final int memory; private final int iterations; private final int parallelism; + private final Semaphore cpuCoreSemaphore; - public Argon2PasswordHashProvider(String version, String type, int hashLength, int memory, int iterations, int parallelism) { + public Argon2PasswordHashProvider(String version, String type, int hashLength, int memory, int iterations, int parallelism, Semaphore cpuCoreSemaphore) { this.version = version; this.type = type; this.hashLength = hashLength; this.memory = memory; this.iterations = iterations; this.parallelism = parallelism; + this.cpuCoreSemaphore = cpuCoreSemaphore; } @Override @@ -98,19 +101,29 @@ public class Argon2PasswordHashProvider implements PasswordHashProvider { } private String encode(String rawPassword, byte[] salt, String version, String type, int hashLength, int parallelism, int memory, int iterations) { - org.bouncycastle.crypto.params.Argon2Parameters parameters = new org.bouncycastle.crypto.params.Argon2Parameters.Builder(Argon2Parameters.getTypeValue(type)) - .withVersion(Argon2Parameters.getVersionValue(version)) - .withSalt(salt) - .withParallelism(parallelism) - .withMemoryAsKB(memory) - .withIterations(iterations).build(); + try { + try { + cpuCoreSemaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + org.bouncycastle.crypto.params.Argon2Parameters parameters = new org.bouncycastle.crypto.params.Argon2Parameters.Builder(Argon2Parameters.getTypeValue(type)) + .withVersion(Argon2Parameters.getVersionValue(version)) + .withSalt(salt) + .withParallelism(parallelism) + .withMemoryAsKB(memory) + .withIterations(iterations).build(); - Argon2BytesGenerator generator = new Argon2BytesGenerator(); - generator.init(parameters); + Argon2BytesGenerator generator = new Argon2BytesGenerator(); + generator.init(parameters); - byte[] result = new byte[hashLength]; - generator.generateBytes(rawPassword.toCharArray(), result); - return Base64.encodeBytes(result); + byte[] result = new byte[hashLength]; + generator.generateBytes(rawPassword.toCharArray(), result); + return Base64.encodeBytes(result); + } finally { + cpuCoreSemaphore.release(); + } } private boolean checkCredData(String key, int expectedValue, PasswordCredentialData data) { diff --git a/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProviderFactory.java b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProviderFactory.java index 220314155b5..2d6bdea9457 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProviderFactory.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/hash/Argon2PasswordHashProviderFactory.java @@ -12,6 +12,7 @@ import org.keycloak.provider.ProviderConfigurationBuilder; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.Semaphore; public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFactory, EnvironmentDependentProviderFactory { @@ -22,6 +23,14 @@ public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFa public static final String MEMORY_KEY = "memory"; public static final String ITERATIONS_KEY = "iterations"; public static final String PARALLELISM_KEY = "parallelism"; + public static final String CPU_CORES_KEY = "cpuCores"; + + /** + * The Argon2 password hashing is CPU bound, so it doesn't make sense to hash more values concurrently than there are cores on the machine. + * When we run more, this only leads to an increased memory usage and to throttling of the process in containerized environments + * when a CPU limit is imposed. The throttling would have a negative impact on other concurrent non-hashing activities of Keycloak. + */ + private Semaphore cpuCoreSempahore; private String version; private String type; @@ -32,7 +41,7 @@ public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFa @Override public PasswordHashProvider create(KeycloakSession session) { - return new Argon2PasswordHashProvider(version, type, hashLength, memory, iterations, parallelism); + return new Argon2PasswordHashProvider(version, type, hashLength, memory, iterations, parallelism, cpuCoreSempahore); } @Override @@ -43,6 +52,7 @@ public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFa memory = config.getInt(MEMORY_KEY, Argon2Parameters.DEFAULT_MEMORY); iterations = config.getInt(ITERATIONS_KEY, Argon2Parameters.DEFAULT_ITERATIONS); parallelism = config.getInt(PARALLELISM_KEY, Argon2Parameters.DEFAULT_PARALLELISM); + cpuCoreSempahore = new Semaphore(config.getInt(CPU_CORES_KEY, Runtime.getRuntime().availableProcessors())); } @Override @@ -106,6 +116,12 @@ public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFa .defaultValue(Argon2Parameters.DEFAULT_PARALLELISM) .add(); + builder.property() + .name(CPU_CORES_KEY) + .type("int") + .helpText("Maximum parallel CPU cores to use for hashing") + .add(); + return builder.build(); } diff --git a/docs/documentation/release_notes/topics/25_0_0.adoc b/docs/documentation/release_notes/topics/25_0_0.adoc index 3c3be29a0f4..d54ede2ee42 100644 --- a/docs/documentation/release_notes/topics/25_0_0.adoc +++ b/docs/documentation/release_notes/topics/25_0_0.adoc @@ -18,6 +18,7 @@ In {project_name} 24 the default hashing iterations for PBKDF2 were increased fr 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 {project_name} requires 7MB per-hashing request. +To prevent excessive memory and CPU usage, the parallel computation of hashes by Argon2 is by default limited to the number of cores available to the JVM. = Cookies updates diff --git a/docs/documentation/server_admin/topics/authentication/password-policies.adoc b/docs/documentation/server_admin/topics/authentication/password-policies.adoc index 2fb37dda37b..d5f7f1c98e2 100644 --- a/docs/documentation/server_admin/topics/authentication/password-policies.adoc +++ b/docs/documentation/server_admin/topics/authentication/password-policies.adoc @@ -40,6 +40,9 @@ at the same time being more secure. The default password hashing algorithm for the server can be configured with `--spi-password-hashing-provider-default=`. +To prevent excessive memory and CPU usage, the parallel computation of hashes by Argon2 is by default limited to the number of cores available to the JVM. +To configure the Argon2 hashing provider, use its provider options. + See the link:{developerguide_link}[{developerguide_name}] on how to add your own hashing algorithm. [NOTE]