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
This commit is contained in:
Thomas Recloux 2017-11-15 10:22:30 +01:00 committed by Stian Thorgersen
parent 4ba72e2d2d
commit 71e0b00600
3 changed files with 61 additions and 10 deletions

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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 + ")");