Limit the concurrency of password hashing to the number of CPU cores available

Closes #28477

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Alexander Schwartz 2024-04-05 19:09:02 +02:00 committed by Alexander Schwartz
parent 58398d1f69
commit 5b4a69a6e9
4 changed files with 46 additions and 13 deletions

View file

@ -15,6 +15,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.MEMORY_KEY;
import static org.keycloak.crypto.hash.Argon2PasswordHashProviderFactory.PARALLELISM_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 memory;
private final int iterations; private final int iterations;
private final int parallelism; 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.version = version;
this.type = type; this.type = type;
this.hashLength = hashLength; this.hashLength = hashLength;
this.memory = memory; this.memory = memory;
this.iterations = iterations; this.iterations = iterations;
this.parallelism = parallelism; this.parallelism = parallelism;
this.cpuCoreSemaphore = cpuCoreSemaphore;
} }
@Override @Override
@ -98,6 +101,13 @@ public class Argon2PasswordHashProvider implements PasswordHashProvider {
} }
private String encode(String rawPassword, byte[] salt, String version, String type, int hashLength, int parallelism, int memory, int iterations) { private String encode(String rawPassword, byte[] salt, String version, String type, int hashLength, int parallelism, int memory, int iterations) {
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)) org.bouncycastle.crypto.params.Argon2Parameters parameters = new org.bouncycastle.crypto.params.Argon2Parameters.Builder(Argon2Parameters.getTypeValue(type))
.withVersion(Argon2Parameters.getVersionValue(version)) .withVersion(Argon2Parameters.getVersionValue(version))
.withSalt(salt) .withSalt(salt)
@ -111,6 +121,9 @@ public class Argon2PasswordHashProvider implements PasswordHashProvider {
byte[] result = new byte[hashLength]; byte[] result = new byte[hashLength];
generator.generateBytes(rawPassword.toCharArray(), result); generator.generateBytes(rawPassword.toCharArray(), result);
return Base64.encodeBytes(result); return Base64.encodeBytes(result);
} finally {
cpuCoreSemaphore.release();
}
} }
private boolean checkCredData(String key, int expectedValue, PasswordCredentialData data) { private boolean checkCredData(String key, int expectedValue, PasswordCredentialData data) {

View file

@ -12,6 +12,7 @@ import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.Semaphore;
public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFactory, EnvironmentDependentProviderFactory { 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 MEMORY_KEY = "memory";
public static final String ITERATIONS_KEY = "iterations"; public static final String ITERATIONS_KEY = "iterations";
public static final String PARALLELISM_KEY = "parallelism"; 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 version;
private String type; private String type;
@ -32,7 +41,7 @@ public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFa
@Override @Override
public PasswordHashProvider create(KeycloakSession session) { 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 @Override
@ -43,6 +52,7 @@ public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFa
memory = config.getInt(MEMORY_KEY, Argon2Parameters.DEFAULT_MEMORY); memory = config.getInt(MEMORY_KEY, Argon2Parameters.DEFAULT_MEMORY);
iterations = config.getInt(ITERATIONS_KEY, Argon2Parameters.DEFAULT_ITERATIONS); iterations = config.getInt(ITERATIONS_KEY, Argon2Parameters.DEFAULT_ITERATIONS);
parallelism = config.getInt(PARALLELISM_KEY, Argon2Parameters.DEFAULT_PARALLELISM); parallelism = config.getInt(PARALLELISM_KEY, Argon2Parameters.DEFAULT_PARALLELISM);
cpuCoreSempahore = new Semaphore(config.getInt(CPU_CORES_KEY, Runtime.getRuntime().availableProcessors()));
} }
@Override @Override
@ -106,6 +116,12 @@ public class Argon2PasswordHashProviderFactory implements PasswordHashProviderFa
.defaultValue(Argon2Parameters.DEFAULT_PARALLELISM) .defaultValue(Argon2Parameters.DEFAULT_PARALLELISM)
.add(); .add();
builder.property()
.name(CPU_CORES_KEY)
.type("int")
.helpText("Maximum parallel CPU cores to use for hashing")
.add();
return builder.build(); return builder.build();
} }

View file

@ -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 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 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.
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 = Cookies updates

View file

@ -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=<algorithm>`. The default password hashing algorithm for the server can be configured with `--spi-password-hashing-provider-default=<algorithm>`.
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. See the link:{developerguide_link}[{developerguide_name}] on how to add your own hashing algorithm.
[NOTE] [NOTE]