Add PBKDF2 encoder support for storing passwords
This commit is contained in:
parent
ef6a0ed8e1
commit
4a53116f2e
3 changed files with 114 additions and 3 deletions
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package org.keycloak.models.jpa;
|
package org.keycloak.models.jpa;
|
||||||
|
|
||||||
import org.bouncycastle.openssl.PEMWriter;
|
import org.bouncycastle.openssl.PEMWriter;
|
||||||
|
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
|
||||||
import org.keycloak.util.PemUtils;
|
import org.keycloak.util.PemUtils;
|
||||||
import org.keycloak.models.ApplicationModel;
|
import org.keycloak.models.ApplicationModel;
|
||||||
import org.keycloak.models.OAuthClientModel;
|
import org.keycloak.models.OAuthClientModel;
|
||||||
|
@ -12,7 +13,6 @@ import org.keycloak.models.SocialLinkModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.jpa.entities.*;
|
import org.keycloak.models.jpa.entities.*;
|
||||||
import org.keycloak.models.utils.SHAPasswordEncoder;
|
|
||||||
import org.keycloak.models.utils.TimeBasedOTP;
|
import org.keycloak.models.utils.TimeBasedOTP;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
|
@ -28,6 +28,7 @@ import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @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) {
|
public boolean validatePassword(UserModel user, String password) {
|
||||||
for (CredentialEntity cred : ((UserAdapter)user).getUser().getCredentials()) {
|
for (CredentialEntity cred : ((UserAdapter)user).getUser().getCredentials()) {
|
||||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
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;
|
return false;
|
||||||
|
@ -1056,7 +1057,9 @@ public class RealmAdapter implements RealmModel {
|
||||||
userEntity.getCredentials().add(credentialEntity);
|
userEntity.getCredentials().add(credentialEntity);
|
||||||
}
|
}
|
||||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
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 {
|
} else {
|
||||||
credentialEntity.setValue(cred.getValue());
|
credentialEntity.setValue(cred.getValue());
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ public class CredentialEntity {
|
||||||
protected String type;
|
protected String type;
|
||||||
protected String value;
|
protected String value;
|
||||||
protected String device;
|
protected String device;
|
||||||
|
protected byte[] salt;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
protected UserEntity user;
|
protected UserEntity user;
|
||||||
|
@ -67,4 +68,14 @@ public class CredentialEntity {
|
||||||
public void setUser(UserEntity user) {
|
public void setUser(UserEntity user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getSalt() {
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSalt(byte[] salt) {
|
||||||
|
this.salt = salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue