keycloak-aplcache

Merge pull request #171 from abstractj/password Add PBKDF2

1/27/2014 11:39:24 AM

Details

diff --git a/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java b/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java
new file mode 100644
index 0000000..b11266f
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java
@@ -0,0 +1,97 @@
+package org.keycloak.models.utils;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+
+/**
+ * <p>
+ * Encoder that uses PBKDF2 function to cryptographically derive passwords.
+ * </p>
+ * <p>Passwords are returned with a Base64 encoding.</p>
+ *
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
+ *
+ */
+public class Pbkdf2PasswordEncoder {
+
+    public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
+    public static final String RNG_ALGORITHM = "SHA1PRNG";
+
+    private static final int DERIVED_KEY_SIZE = 512;
+    private static final int ITERATIONS = 20000;
+
+    private final int iterations;
+    private byte[] salt;
+
+    public Pbkdf2PasswordEncoder(byte[] salt, int iterations) {
+        this.salt = salt;
+        this.iterations = iterations;
+    }
+
+    public Pbkdf2PasswordEncoder(byte[] salt) {
+        this(salt, ITERATIONS);
+    }
+
+    /**
+     * Encode the raw password provided
+     * @param rawPassword The password used as a master key to derive into a session key
+     * @return encoded password in Base64
+     */
+    public String encode(String rawPassword) {
+
+        String encodedPassword;
+
+        KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, DERIVED_KEY_SIZE);
+
+        try {
+            byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded();
+            encodedPassword = Base64.encodeBytes(key);
+        } catch (InvalidKeySpecException e) {
+            throw new RuntimeException("Credential could not be encoded");
+        }
+
+        return encodedPassword;
+    }
+
+    /**
+     * Encode the password provided and compare with the hash stored into the database
+     * @param rawPassword The password provided
+     * @param encodedPassword Encoded hash stored into the database
+     * @return true if the password is valid, otherwise false for invalid credentials
+     */
+    public boolean verify(String rawPassword, String encodedPassword) {
+        return encode(rawPassword).equals(encodedPassword);
+    }
+
+    /**
+     * Generate a salt for each password
+     * @return cryptographically strong random number
+     */
+    public static byte[] getSalt() {
+        byte[] buffer = new byte[16];
+
+        SecureRandom secureRandom;
+
+        try {
+            secureRandom = SecureRandom.getInstance(RNG_ALGORITHM);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("RNG algorithm not found");
+        }
+
+        secureRandom.nextBytes(buffer);
+
+        return buffer;
+    }
+
+    private static SecretKeyFactory getSecretKeyFactory() {
+        try {
+            return SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("PBKDF2 algorithm not found");
+        }
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java
index 64cf3cf..ec110b8 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java
@@ -24,6 +24,7 @@ public class CredentialEntity {
     protected String type;
     protected String value;
     protected String device;
+    protected byte[] salt;
 
     @ManyToOne
     protected UserEntity user;
@@ -67,4 +68,14 @@ public class CredentialEntity {
     public void setUser(UserEntity user) {
         this.user = user;
     }
+
+    public byte[] getSalt() {
+        return salt;
+    }
+
+    public void setSalt(byte[] salt) {
+        this.salt = salt;
+    }
+
+
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 9b9679d..25352ca 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1,6 +1,7 @@
 package org.keycloak.models.jpa;
 
 import org.bouncycastle.openssl.PEMWriter;
+import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
 import org.keycloak.util.PemUtils;
 import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.OAuthClientModel;
@@ -12,7 +13,6 @@ import org.keycloak.models.SocialLinkModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.jpa.entities.*;
-import org.keycloak.models.utils.SHAPasswordEncoder;
 import org.keycloak.models.utils.TimeBasedOTP;
 
 import javax.persistence.EntityManager;
@@ -28,6 +28,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.*;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -1021,7 +1022,7 @@ public class RealmAdapter implements RealmModel {
     public boolean validatePassword(UserModel user, String password) {
         for (CredentialEntity cred : ((UserAdapter)user).getUser().getCredentials()) {
             if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
-                return new SHAPasswordEncoder(512).verify(password, cred.getValue());
+                return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue());
             }
         }
         return false;
@@ -1056,7 +1057,9 @@ public class RealmAdapter implements RealmModel {
             userEntity.getCredentials().add(credentialEntity);
         }
         if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
-            credentialEntity.setValue(new SHAPasswordEncoder(512).encode(cred.getValue()));
+            byte[] salt = getSalt();
+            credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue()));
+            credentialEntity.setSalt(salt);
         } else {
             credentialEntity.setValue(cred.getValue());
         }