diff --git a/server-spi-private/pom.xml b/server-spi-private/pom.xml
index f241ca8d87e..36878b1c5d8 100755
--- a/server-spi-private/pom.xml
+++ b/server-spi-private/pom.xml
@@ -86,6 +86,11 @@
junit
test
+
+ org.hamcrest
+ hamcrest-all
+ test
+
diff --git a/server-spi-private/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java b/server-spi-private/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java
index e71ff6d842c..9c146f0908c 100644
--- a/server-spi-private/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java
@@ -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);
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/PasswordHashingTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/PasswordHashingTest.java
index 8b978a40587..837c41314f4 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/PasswordHashingTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/PasswordHashingTest.java
@@ -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 + ")");