configurable hash iterations

This commit is contained in:
Bill Burke 2014-07-07 12:11:45 -04:00
parent 4cbc423d67
commit 15d7568792
14 changed files with 165 additions and 28 deletions

View file

@ -908,6 +908,7 @@ module.factory('PasswordPolicy', function() {
var p = {};
p.policyMessages = {
hashIterations: "Number of hashing iterations. Default is 1. Recommended is 50000.",
length: "Minimal password length (integer type). Default value is 8.",
digits: "Minimal number (integer type) of digits in password. Default value is 1.",
lowerCase: "Minimal number (integer type) of lowercase characters in password. Default value is 1.",
@ -916,6 +917,7 @@ module.factory('PasswordPolicy', function() {
}
p.allPolicies = [
{ name: 'hashIterations', value: 1 },
{ name: 'length', value: 8 },
{ name: 'digits', value: 1 },
{ name: 'lowerCase', value: 1 },

View file

@ -50,7 +50,7 @@
<input class="form-control disabled" type="text" value="{{p.name|capitalize}}" readonly>
</td>
<td>
<input class="form-control" ng-model="p.value" type="number" placeholder="No value assigned" min="1" max="50">
<input class="form-control" ng-model="p.value" type="number" placeholder="No value assigned" min="1">
</td>
<td class="actions">
<div class="action-div"><i class="pficon pficon-delete" ng-click="removePolicy($index)" tooltip-placement="right" tooltip="Remove Policy"></i></div>

View file

@ -0,0 +1,53 @@
package org.keycloak.models;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class CredentialValidation {
private static int hashIterations(RealmModel realm) {
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
return policy.getHashIterations();
}
return -1;
}
/**
* Will update password if hash iteration policy has changed
*
* @param realm
* @param user
* @param password
* @return
*/
public static boolean validatePassword(RealmModel realm, UserModel user, String password) {
boolean validated = false;
UserCredentialValueModel passwordCred = null;
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
validated = new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue(), cred.getHashIterations());
passwordCred = cred;
}
}
if (validated) {
int iterations = hashIterations(realm);
if (iterations > -1 && iterations != passwordCred.getHashIterations()) {
UserCredentialValueModel newCred = new UserCredentialValueModel();
newCred.setType(passwordCred.getType());
newCred.setDevice(passwordCred.getDevice());
newCred.setSalt(passwordCred.getSalt());
newCred.setHashIterations(iterations);
newCred.setValue(new Pbkdf2PasswordEncoder(newCred.getSalt()).encode(password, iterations));
user.updateCredentialDirectly(newCred);
}
}
return validated;
}
}

View file

@ -52,11 +52,28 @@ public class PasswordPolicy {
list.add(new UpperCase(args));
} else if (name.equals(SpecialChars.NAME)) {
list.add(new SpecialChars(args));
} else if (name.equals(HashIterations.NAME)) {
list.add(new HashIterations(args));
}
}
return list;
}
/**
*
* @return -1 if no hash iterations setting
*/
public int getHashIterations() {
if (policies == null) return -1;
for (Policy p : policies) {
if (p instanceof HashIterations) {
return ((HashIterations)p).iterations;
}
}
return -1;
}
public String validate(String password) {
for (Policy p : policies) {
String error = p.validate(password);
@ -71,6 +88,20 @@ public class PasswordPolicy {
public String validate(String password);
}
private static class HashIterations implements Policy {
private static final String NAME = "hashIterations";
private int iterations;
public HashIterations(String[] args) {
iterations = intArg(NAME, 1, args);
}
@Override
public String validate(String password) {
return null;
}
}
private static class Length implements Policy {
private static final String NAME = "length";
private int min;

View file

@ -11,6 +11,7 @@ public class UserCredentialValueModel {
private String value;
private String device;
private byte[] salt;
private int hashIterations;
public String getType() {
return type;
@ -43,4 +44,12 @@ public class UserCredentialValueModel {
public void setSalt(byte[] salt) {
this.salt = salt;
}
public int getHashIterations() {
return hashIterations;
}
public void setHashIterations(int iterations) {
this.hashIterations = iterations;
}
}

View file

@ -9,6 +9,7 @@ public class CredentialEntity {
private String value;
private String device;
private byte[] salt;
private int hashIterations;
public String getType() {
return type;
@ -41,4 +42,12 @@ public class CredentialEntity {
public void setSalt(byte[] salt) {
this.salt = salt;
}
public int getHashIterations() {
return hashIterations;
}
public void setHashIterations(int hashIterations) {
this.hashIterations = hashIterations;
}
}

View file

@ -43,7 +43,7 @@ public class Pbkdf2PasswordEncoder {
* @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) {
public String encode(String rawPassword, int iterations) {
String encodedPassword;
@ -59,6 +59,10 @@ public class Pbkdf2PasswordEncoder {
return encodedPassword;
}
public String encode(String rawPassword) {
return encode(rawPassword, iterations);
}
/**
* Encode the password provided and compare with the hash stored into the database
* @param rawPassword The password provided
@ -69,6 +73,16 @@ public class Pbkdf2PasswordEncoder {
return encode(rawPassword).equals(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, int iterations) {
return encode(rawPassword, iterations).equals(encodedPassword);
}
/**
* Generate a salt for each password
* @return cryptographically strong random number

View file

@ -3,6 +3,7 @@ package org.keycloak.models.cache;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.AuthenticationProviderModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidation;
import org.keycloak.models.OAuthClientModel;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
@ -380,13 +381,7 @@ public class RealmAdapter implements RealmModel {
@Override
public boolean validatePassword(UserModel user, String password) {
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue());
}
}
return false;
return CredentialValidation.validatePassword(this, user, password);
}
@Override

View file

@ -3,6 +3,7 @@ package org.keycloak.models.jpa;
import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.AuthenticationProviderModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidation;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RoleContainerModel;
@ -962,19 +963,9 @@ public class RealmAdapter implements RealmModel {
return role.getContainer().removeRole(role);
}
@Override
public boolean validatePassword(UserModel user, String password) {
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue());
}
}
return false;
return CredentialValidation.validatePassword(this, user, password);
}
@Override

View file

@ -3,6 +3,7 @@ package org.keycloak.models.jpa;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
@ -197,8 +198,15 @@ public class UserAdapter implements UserModel {
}
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
byte[] salt = getSalt();
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue()));
int hashIterations = 1;
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1) hashIterations = 1;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} else {
credentialEntity.setValue(cred.getValue());
}
@ -228,6 +236,7 @@ public class UserAdapter implements UserModel {
credModel.setDevice(credEntity.getDevice());
credModel.setValue(credEntity.getValue());
credModel.setSalt(credEntity.getSalt());
credModel.setHashIterations(credEntity.getHashIterations());
result.add(credModel);
}
@ -251,6 +260,7 @@ public class UserAdapter implements UserModel {
credentialEntity.setValue(credModel.getValue());
credentialEntity.setSalt(credModel.getSalt());
credentialEntity.setDevice(credModel.getDevice());
credentialEntity.setHashIterations(credModel.getHashIterations());
em.flush();
}

View file

@ -28,6 +28,7 @@ public class CredentialEntity {
protected String value;
protected String device;
protected byte[] salt;
protected int hashIterations;
@ManyToOne(fetch = FetchType.LAZY)
protected UserEntity user;
@ -80,5 +81,11 @@ public class CredentialEntity {
this.salt = salt;
}
public int getHashIterations() {
return hashIterations;
}
public void setHashIterations(int hashIterations) {
this.hashIterations = hashIterations;
}
}

View file

@ -8,6 +8,7 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.AuthenticationProviderModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidation;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuthClientModel;
import org.keycloak.models.PasswordPolicy;
@ -808,12 +809,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public boolean validatePassword(UserModel user, String password) {
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue());
}
}
return false;
return CredentialValidation.validatePassword(this, user, password);
}
@Override

View file

@ -4,6 +4,7 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
@ -199,8 +200,15 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
}
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
byte[] salt = Pbkdf2PasswordEncoder.getSalt();
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue()));
int hashIterations = 1;
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1) hashIterations = 1;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} else {
credentialEntity.setValue(cred.getValue());
}
@ -229,6 +237,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
credModel.setDevice(credEntity.getDevice());
credModel.setValue(credEntity.getValue());
credModel.setSalt(credEntity.getSalt());
credModel.setHashIterations(credEntity.getHashIterations());
result.add(credModel);
}
@ -249,6 +258,8 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
credentialEntity.setValue(credModel.getValue());
credentialEntity.setSalt(credModel.getSalt());
credentialEntity.setDevice(credModel.getDevice());
credentialEntity.setHashIterations(credModel.getHashIterations());
getMongoStore().updateEntity(user, invocationContext);
}

View file

@ -8,11 +8,13 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.OAuthClientModel;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.idm.CredentialRepresentation;
@ -135,6 +137,13 @@ public class AdapterTest extends AbstractModelTest {
cred.setValue("geheim");
user.updateCredential(cred);
Assert.assertTrue(realmModel.validatePassword(user, "geheim"));
List<UserCredentialValueModel> creds = user.getCredentialsDirectly();
Assert.assertEquals(creds.get(0).getHashIterations(), 1);
realmModel.setPasswordPolicy( new PasswordPolicy("hashIterations(200)"));
Assert.assertTrue(realmModel.validatePassword(user, "geheim"));
creds = user.getCredentialsDirectly();
Assert.assertEquals(creds.get(0).getHashIterations(), 200);
realmModel.setPasswordPolicy( new PasswordPolicy("hashIterations(1)"));
}
@Test