keycloak-aplcache

KEYCLOAK-5857 Supports PBKDF2 hashes with different key size The

11/15/2017 7:22:30 AM

Details

diff --git a/server-spi-private/pom.xml b/server-spi-private/pom.xml
index f241ca8..36878b1 100755
--- a/server-spi-private/pom.xml
+++ b/server-spi-private/pom.xml
@@ -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>
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 e71ff6d..9c146f0 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 8b978a4..837c413 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;
@@ -189,6 +190,36 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
     }
 
     @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 + ")");
         String username = "testPbkdf2Sha1";