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.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) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Reference in a new issue