From 71e0b006000bee4e325b8635daa321c53a84f3f1 Mon Sep 17 00:00:00 2001 From: Thomas Recloux Date: Wed, 15 Nov 2017 10:22:30 +0100 Subject: [PATCH] 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 --- server-spi-private/pom.xml | 5 +++ .../hash/Pbkdf2PasswordHashProvider.java | 35 +++++++++++++------ .../testsuite/forms/PasswordHashingTest.java | 31 ++++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/server-spi-private/pom.xml b/server-spi-private/pom.xml index f241ca8d87..36878b1c5d 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 e71ff6d842..9c146f0908 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 8b978a4058..837c41314f 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 + ")");