KEYCLOAK-5857 Supports PBKDF2 hashes with different key size
The original use case is to support imported credentials with a different key size without implementing a totally new PasswordHashProvider
This commit is contained in:
parent
4ba72e2d2d
commit
71e0b00600
3 changed files with 61 additions and 10 deletions
|
@ -86,6 +86,11 @@
|
|||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-all</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.models.UserCredentialModel;
|
|||
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
@ -37,14 +38,18 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
|||
private final String providerId;
|
||||
|
||||
private final String pbkdf2Algorithm;
|
||||
private int defaultIterations;
|
||||
|
||||
public static final int DERIVED_KEY_SIZE = 512;
|
||||
private final int defaultIterations;
|
||||
private final int derivedKeySize;
|
||||
public static final int DEFAULT_DERIVED_KEY_SIZE = 512;
|
||||
|
||||
public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm, int defaultIterations) {
|
||||
this(providerId, pbkdf2Algorithm, defaultIterations, DEFAULT_DERIVED_KEY_SIZE);
|
||||
}
|
||||
public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm, int defaultIterations, int derivedKeySize) {
|
||||
this.providerId = providerId;
|
||||
this.pbkdf2Algorithm = pbkdf2Algorithm;
|
||||
this.defaultIterations = defaultIterations;
|
||||
this.derivedKeySize = derivedKeySize;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -54,7 +59,9 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
|||
policyHashIterations = defaultIterations;
|
||||
}
|
||||
|
||||
return credential.getHashIterations() == policyHashIterations && providerId.equals(credential.getAlgorithm());
|
||||
return credential.getHashIterations() == policyHashIterations
|
||||
&& providerId.equals(credential.getAlgorithm())
|
||||
&& derivedKeySize == keySize(credential);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,7 +71,7 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
|||
}
|
||||
|
||||
byte[] salt = getSalt();
|
||||
String encodedPassword = encode(rawPassword, iterations, salt);
|
||||
String encodedPassword = encode(rawPassword, iterations, salt, derivedKeySize);
|
||||
|
||||
credential.setAlgorithm(providerId);
|
||||
credential.setType(UserCredentialModel.PASSWORD);
|
||||
|
@ -80,19 +87,28 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
|||
}
|
||||
|
||||
byte[] salt = getSalt();
|
||||
return encode(rawPassword, iterations, salt);
|
||||
return encode(rawPassword, iterations, salt, derivedKeySize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String rawPassword, CredentialModel credential) {
|
||||
return encode(rawPassword, credential.getHashIterations(), credential.getSalt()).equals(credential.getValue());
|
||||
return encode(rawPassword, credential.getHashIterations(), credential.getSalt(), keySize(credential)).equals(credential.getValue());
|
||||
}
|
||||
|
||||
private int keySize(CredentialModel credential) {
|
||||
try {
|
||||
byte[] bytes = Base64.decode(credential.getValue());
|
||||
return bytes.length * 8;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Credential could not be decoded", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
}
|
||||
|
||||
private String encode(String rawPassword, int iterations, byte[] salt) {
|
||||
KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, DERIVED_KEY_SIZE);
|
||||
private String encode(String rawPassword, int iterations, byte[] salt, int derivedKeySize) {
|
||||
KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, derivedKeySize);
|
||||
|
||||
try {
|
||||
byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded();
|
||||
|
@ -100,7 +116,6 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
|||
} catch (InvalidKeySpecException e) {
|
||||
throw new RuntimeException("Credential could not be encoded", e);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.OAuth2Constants;
|
|||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.credential.hash.Pbkdf2PasswordHashProvider;
|
||||
import org.keycloak.credential.hash.Pbkdf2PasswordHashProviderFactory;
|
||||
import org.keycloak.credential.hash.Pbkdf2Sha256PasswordHashProviderFactory;
|
||||
import org.keycloak.credential.hash.Pbkdf2Sha512PasswordHashProviderFactory;
|
||||
|
@ -188,6 +189,36 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
|||
assertArrayEquals(salt, credential.getSalt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordRehashedWhenCredentialImportedWithDifferentKeySize() throws Exception {
|
||||
setPasswordPolicy("hashAlgorithm(" + Pbkdf2Sha512PasswordHashProviderFactory.ID + ") and hashIterations("+ Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS + ")");
|
||||
|
||||
String username = "testPasswordRehashedWhenCredentialImportedWithDifferentKeySize";
|
||||
String password = "password";
|
||||
|
||||
// Encode with a specific key size ( 256 instead of default: 512)
|
||||
Pbkdf2PasswordHashProvider specificKeySizeHashProvider = new Pbkdf2PasswordHashProvider(Pbkdf2Sha512PasswordHashProviderFactory.ID,
|
||||
Pbkdf2Sha512PasswordHashProviderFactory.PBKDF2_ALGORITHM,
|
||||
Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS,
|
||||
256);
|
||||
String encodedPassword = specificKeySizeHashProvider.encode(password, -1);
|
||||
|
||||
// Create a user with the encoded password, simulating a user import from a different system using a specific key size
|
||||
CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
|
||||
credentialRepresentation.setAlgorithm(Pbkdf2Sha512PasswordHashProviderFactory.PBKDF2_ALGORITHM);
|
||||
credentialRepresentation.setHashedSaltedValue(encodedPassword);
|
||||
UserRepresentation user = UserBuilder.create().username(username).password(encodedPassword).build();
|
||||
ApiUtil.createUserWithAdminClient(adminClient.realm("test"),user);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login(username, password);
|
||||
|
||||
CredentialModel postLoginCredentials = fetchCredentials(username);
|
||||
assertEquals(encodedPassword.length() * 2, postLoginCredentials.getValue().length());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testPbkdf2Sha1() throws Exception {
|
||||
setPasswordPolicy("hashAlgorithm(" + Pbkdf2PasswordHashProviderFactory.ID + ")");
|
||||
|
|
Loading…
Reference in a new issue