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:
parent
58398d1f69
commit
5b4a69a6e9
4 changed files with 46 additions and 13 deletions
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>`.
|
||||
|
||||
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]
|
||||
|
|
Loading…
Reference in a new issue