Short passwords with PBKDF2 mode working (#14437)
* Short passwords with PBKDF2 mode working Closes #14314 * Add config option to Pbkdf2 provider to control max padding * Update according to PR review - more testing for padding and for non-fips mode
This commit is contained in:
parent
e4a76bacb1
commit
c0c0d3a6ba
11 changed files with 407 additions and 54 deletions
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
*
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.common.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class PaddingUtils {
|
||||||
|
|
||||||
|
private static final char PADDING_CHAR_NONE = '\u0000';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies padding to given string up to specified number of characters. If given string is shorter or same as maxPaddingLength, it will just return the original string.
|
||||||
|
* Otherwise it would be padded with "\0" character to have at least "maxPaddingLength" characters
|
||||||
|
*
|
||||||
|
* @param rawString raw string
|
||||||
|
* @param maxPaddingLength max padding length
|
||||||
|
* @return padded output
|
||||||
|
*/
|
||||||
|
public static String padding(String rawString, int maxPaddingLength) {
|
||||||
|
if (rawString.length() < maxPaddingLength) {
|
||||||
|
int nPad = maxPaddingLength - rawString.length();
|
||||||
|
StringBuilder result = new StringBuilder(rawString);
|
||||||
|
for (int i = 0 ; i < nPad; i++) result.append(PADDING_CHAR_NONE);
|
||||||
|
return result.toString();
|
||||||
|
} else
|
||||||
|
return rawString;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
*
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.common.util;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class PaddingUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPadding() {
|
||||||
|
Assert.assertEquals("foo123", PaddingUtils.padding("foo123", 5));
|
||||||
|
Assert.assertEquals("foo123", PaddingUtils.padding("foo123", 6));
|
||||||
|
Assert.assertEquals("foo123\0", PaddingUtils.padding("foo123", 7));
|
||||||
|
|
||||||
|
Assert.assertEquals("someLongPassword", PaddingUtils.padding("someLongPassword", 14));
|
||||||
|
Assert.assertEquals("short\0\0\0\0\0\0\0\0\0", PaddingUtils.padding("short", 14));
|
||||||
|
}
|
||||||
|
}
|
83
core/src/test/java/org/keycloak/rule/RunInThreadRule.java
Normal file
83
core/src/test/java/org/keycloak/rule/RunInThreadRule.java
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package org.keycloak.rule;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import org.junit.rules.TestRule;
|
||||||
|
import org.junit.runner.Description;
|
||||||
|
import org.junit.runners.model.Statement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs every test method in it's own thread. Useful for example for
|
||||||
|
* testing bouncycastle FIPS (BCFIPS cannot switch bouncycastle to non-approved mode after it was switched before in approved mode in the same thread)
|
||||||
|
*
|
||||||
|
* Copy/paste from https://www.codeaffine.com/2014/07/21/a-junit-rule-to-run-a-test-in-its-own-thread/
|
||||||
|
*
|
||||||
|
* RunInThread an other accompanying files are licensed under the MIT
|
||||||
|
* license. Copyright (C) Frank Appel 2016-2021. All rights reserved
|
||||||
|
*/
|
||||||
|
public class RunInThreadRule implements TestRule {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Statement apply(Statement base, Description description ) {
|
||||||
|
Statement result = base;
|
||||||
|
result = new RunInThreadStatement( base );
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class RunInThreadStatement extends Statement {
|
||||||
|
|
||||||
|
private final Statement baseStatement;
|
||||||
|
private Future<?> future;
|
||||||
|
private volatile Throwable throwable;
|
||||||
|
|
||||||
|
RunInThreadStatement( Statement baseStatement ) {
|
||||||
|
this.baseStatement = baseStatement;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void evaluate() throws Throwable {
|
||||||
|
ExecutorService executorService = runInThread();
|
||||||
|
try {
|
||||||
|
waitTillFinished();
|
||||||
|
} finally {
|
||||||
|
executorService.shutdown();
|
||||||
|
}
|
||||||
|
rethrowAssertionsAndErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExecutorService runInThread() {
|
||||||
|
ExecutorService result = Executors.newSingleThreadExecutor();
|
||||||
|
future = result.submit( new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
baseStatement.evaluate();
|
||||||
|
} catch( Throwable throwable ) {
|
||||||
|
RunInThreadStatement.this.throwable = throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitTillFinished() {
|
||||||
|
try {
|
||||||
|
future.get();
|
||||||
|
} catch (ExecutionException shouldNotHappen ) {
|
||||||
|
throw new IllegalStateException( shouldNotHappen );
|
||||||
|
} catch( InterruptedException shouldNotHappen ) {
|
||||||
|
throw new IllegalStateException( shouldNotHappen );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rethrowAssertionsAndErrors() throws Throwable {
|
||||||
|
if( throwable != null ) {
|
||||||
|
throw throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package org.keycloak.crypto.fips.test;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.CryptoServicesRegistrar;
|
||||||
|
import org.bouncycastle.crypto.fips.FipsUnapprovedOperationError;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.common.util.Environment;
|
||||||
|
import org.keycloak.credential.hash.AbstractPbkdf2PasswordHashProviderFactory;
|
||||||
|
import org.keycloak.credential.hash.PasswordHashProvider;
|
||||||
|
import org.keycloak.credential.hash.PasswordHashSpi;
|
||||||
|
import org.keycloak.credential.hash.Pbkdf2Sha256PasswordHashProviderFactory;
|
||||||
|
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||||
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
|
import org.keycloak.rule.RunInThreadRule;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class FIPS1402Pbkdf2PasswordPaddingTest {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(FIPS1402SecureRandomTest.class);
|
||||||
|
|
||||||
|
private static final int ITERATIONS = 27500;
|
||||||
|
|
||||||
|
private static final int BC_FIPS_PADDING_LENGTH = 14;
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public RunInThreadRule runInThread = new RunInThreadRule();
|
||||||
|
|
||||||
|
private static boolean defaultBcFipsApprovedMode;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void checkBcFipsApproved() {
|
||||||
|
defaultBcFipsApprovedMode = CryptoServicesRegistrar.isInApprovedOnlyMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
// Run this test just if java is in FIPS mode
|
||||||
|
Assume.assumeTrue("Java is not in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode());
|
||||||
|
Assert.assertEquals(defaultBcFipsApprovedMode, CryptoServicesRegistrar.isInApprovedOnlyMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShortPassword() {
|
||||||
|
testPasswordVerification("short", false, BC_FIPS_PADDING_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLongPassword() {
|
||||||
|
testPasswordVerification("someLongerPasswordThan14Chars", false, BC_FIPS_PADDING_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate the test for backwards compatibility - password created in non-approved mode should still work after server is restarted to approved mode
|
||||||
|
@Test
|
||||||
|
public void testShortPasswordWithSwitchToApprovedModel() {
|
||||||
|
testPasswordVerification("short", true, BC_FIPS_PADDING_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate the test for backwards compatibility - password created in non-approved mode should still work after server is restarted to approved mode
|
||||||
|
@Test
|
||||||
|
public void testLongPasswordWithSwitchToApprovedModel() {
|
||||||
|
testPasswordVerification("someLongerPasswordThan14Chars", true, BC_FIPS_PADDING_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShortPasswordWithSwitchToApprovedModelAndWithoutPadding() {
|
||||||
|
try {
|
||||||
|
testPasswordVerification("short", true, 0);
|
||||||
|
Assert.fail("Password hashing should fail without padding in BCFIPS approved mode");
|
||||||
|
} catch (FipsUnapprovedOperationError expectedError) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate the test for backwards compatibility - password created in non-approved mode should still work after server is restarted to approved mode
|
||||||
|
@Test
|
||||||
|
public void testLongPasswordWithSwitchToApprovedModelAndWithoutPadding() {
|
||||||
|
testPasswordVerification("someLongerPasswordThan14Chars", true, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void testPasswordVerification(String password, boolean shouldEnableApprovedModeForVerification, int maxPaddingLength) {
|
||||||
|
Pbkdf2Sha256PasswordHashProviderFactory factory = new Pbkdf2Sha256PasswordHashProviderFactory();
|
||||||
|
|
||||||
|
System.setProperty("keycloak." + PasswordHashSpi.NAME + "." + Pbkdf2Sha256PasswordHashProviderFactory.ID + "." + AbstractPbkdf2PasswordHashProviderFactory.MAX_PADDING_LENGTH_PROPERTY,
|
||||||
|
String.valueOf(maxPaddingLength));
|
||||||
|
factory.init(Config.scope(PasswordHashSpi.NAME, Pbkdf2Sha256PasswordHashProviderFactory.ID));
|
||||||
|
|
||||||
|
PasswordHashProvider pbkdf2HashProvider = factory.create(null);
|
||||||
|
|
||||||
|
PasswordCredentialModel passwordCred = pbkdf2HashProvider.encodedCredential(password, ITERATIONS);
|
||||||
|
logger.infof("After password credential created. BC FIPS approved mode: %b, password: %s", CryptoServicesRegistrar.isInApprovedOnlyMode(), password);
|
||||||
|
|
||||||
|
if (shouldEnableApprovedModeForVerification) {
|
||||||
|
CryptoServicesRegistrar.setApprovedOnlyMode(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.infof("Before password verification. BC FIPS approved mode: %b, password: %s", CryptoServicesRegistrar.isInApprovedOnlyMode(), password);
|
||||||
|
Assert.assertThat(true, is(pbkdf2HashProvider.verify(password, passwordCred)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
*
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.credential.hash;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractPbkdf2PasswordHashProviderFactory implements PasswordHashProviderFactory {
|
||||||
|
|
||||||
|
public static final String MAX_PADDING_LENGTH_PROPERTY = "max-padding-length";
|
||||||
|
|
||||||
|
// Minimum password length before password is encoded. If the provided password is shorter than the configured count of characters by this option,
|
||||||
|
// then the padding with '\0' character would be used. By default, it is 0, so no padding used.
|
||||||
|
// This can be used as for example in fips mode (BCFIPS), the pbkdf2 function does not allow less than 14 characters (112 bits).
|
||||||
|
// Regarding backwards compatibility, there is no issue with adding this option against already existing DB of passwords as password value without padding can be verified
|
||||||
|
// against the password with padding as it produces same encoded value.
|
||||||
|
private int maxPaddingLength = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
this.maxPaddingLength = config.getInt(MAX_PADDING_LENGTH_PROPERTY, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxPaddingLength() {
|
||||||
|
return maxPaddingLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxPaddingLength(int maxPaddingLength) {
|
||||||
|
this.maxPaddingLength = maxPaddingLength;
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import org.keycloak.provider.Spi;
|
||||||
*/
|
*/
|
||||||
public class PasswordHashSpi implements Spi {
|
public class PasswordHashSpi implements Spi {
|
||||||
|
|
||||||
|
public static final String NAME = "password-hashing";
|
||||||
@Override
|
@Override
|
||||||
public boolean isInternal() {
|
public boolean isInternal() {
|
||||||
return true;
|
return true;
|
||||||
|
@ -33,7 +34,7 @@ public class PasswordHashSpi implements Spi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "password-hashing";
|
return NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.credential.hash;
|
||||||
|
|
||||||
import org.keycloak.common.crypto.CryptoIntegration;
|
import org.keycloak.common.crypto.CryptoIntegration;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.common.util.PaddingUtils;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||||
|
|
||||||
|
@ -40,16 +41,19 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
||||||
|
|
||||||
private final String pbkdf2Algorithm;
|
private final String pbkdf2Algorithm;
|
||||||
private final int defaultIterations;
|
private final int defaultIterations;
|
||||||
|
|
||||||
|
private final int maxPaddingLength;
|
||||||
private final int derivedKeySize;
|
private final int derivedKeySize;
|
||||||
public static final int DEFAULT_DERIVED_KEY_SIZE = 512;
|
public static final int DEFAULT_DERIVED_KEY_SIZE = 512;
|
||||||
|
|
||||||
public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm, int defaultIterations) {
|
public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm, int defaultIterations, int minPbkdf2PasswordLengthForPadding) {
|
||||||
this(providerId, pbkdf2Algorithm, defaultIterations, DEFAULT_DERIVED_KEY_SIZE);
|
this(providerId, pbkdf2Algorithm, defaultIterations, minPbkdf2PasswordLengthForPadding, DEFAULT_DERIVED_KEY_SIZE);
|
||||||
}
|
}
|
||||||
public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm, int defaultIterations, int derivedKeySize) {
|
public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm, int defaultIterations, int maxPaddingLength, int derivedKeySize) {
|
||||||
this.providerId = providerId;
|
this.providerId = providerId;
|
||||||
this.pbkdf2Algorithm = pbkdf2Algorithm;
|
this.pbkdf2Algorithm = pbkdf2Algorithm;
|
||||||
this.defaultIterations = defaultIterations;
|
this.defaultIterations = defaultIterations;
|
||||||
|
this.maxPaddingLength = maxPaddingLength;
|
||||||
this.derivedKeySize = derivedKeySize;
|
this.derivedKeySize = derivedKeySize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +109,8 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String encodedCredential(String rawPassword, int iterations, byte[] salt, int derivedKeySize) {
|
private String encodedCredential(String rawPassword, int iterations, byte[] salt, int derivedKeySize) {
|
||||||
KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, derivedKeySize);
|
String rawPasswordWithPadding = PaddingUtils.padding(rawPassword, maxPaddingLength);
|
||||||
|
KeySpec spec = new PBEKeySpec(rawPasswordWithPadding.toCharArray(), salt, iterations, derivedKeySize);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded();
|
byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded();
|
||||||
|
|
|
@ -17,14 +17,12 @@
|
||||||
|
|
||||||
package org.keycloak.credential.hash;
|
package org.keycloak.credential.hash;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
||||||
*/
|
*/
|
||||||
public class Pbkdf2PasswordHashProviderFactory implements PasswordHashProviderFactory {
|
public class Pbkdf2PasswordHashProviderFactory extends AbstractPbkdf2PasswordHashProviderFactory implements PasswordHashProviderFactory {
|
||||||
|
|
||||||
public static final String ID = "pbkdf2";
|
public static final String ID = "pbkdf2";
|
||||||
|
|
||||||
|
@ -34,23 +32,11 @@ public class Pbkdf2PasswordHashProviderFactory implements PasswordHashProviderFa
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PasswordHashProvider create(KeycloakSession session) {
|
public PasswordHashProvider create(KeycloakSession session) {
|
||||||
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS);
|
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS, getMaxPaddingLength());
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(Config.Scope config) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
package org.keycloak.credential.hash;
|
package org.keycloak.credential.hash;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PBKDF2 Password Hash provider with HMAC using SHA256
|
* PBKDF2 Password Hash provider with HMAC using SHA256
|
||||||
*
|
*
|
||||||
* @author <a href"mailto:abkaplan07@gmail.com">Adam Kaplan</a>
|
* @author <a href"mailto:abkaplan07@gmail.com">Adam Kaplan</a>
|
||||||
*/
|
*/
|
||||||
public class Pbkdf2Sha256PasswordHashProviderFactory implements PasswordHashProviderFactory {
|
public class Pbkdf2Sha256PasswordHashProviderFactory extends AbstractPbkdf2PasswordHashProviderFactory implements PasswordHashProviderFactory {
|
||||||
|
|
||||||
public static final String ID = "pbkdf2-sha256";
|
public static final String ID = "pbkdf2-sha256";
|
||||||
|
|
||||||
|
@ -19,23 +17,11 @@ public class Pbkdf2Sha256PasswordHashProviderFactory implements PasswordHashProv
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PasswordHashProvider create(KeycloakSession session) {
|
public PasswordHashProvider create(KeycloakSession session) {
|
||||||
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS);
|
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS, getMaxPaddingLength());
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(Config.Scope config) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
package org.keycloak.credential.hash;
|
package org.keycloak.credential.hash;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider factory for SHA512 variant of the PBKDF2 password hash algorithm.
|
* Provider factory for SHA512 variant of the PBKDF2 password hash algorithm.
|
||||||
*
|
*
|
||||||
* @author @author <a href="mailto:abkaplan07@gmail.com">Adam Kaplan</a>
|
* @author @author <a href="mailto:abkaplan07@gmail.com">Adam Kaplan</a>
|
||||||
*/
|
*/
|
||||||
public class Pbkdf2Sha512PasswordHashProviderFactory implements PasswordHashProviderFactory {
|
public class Pbkdf2Sha512PasswordHashProviderFactory extends AbstractPbkdf2PasswordHashProviderFactory implements PasswordHashProviderFactory {
|
||||||
|
|
||||||
public static final String ID = "pbkdf2-sha512";
|
public static final String ID = "pbkdf2-sha512";
|
||||||
|
|
||||||
|
@ -19,23 +17,11 @@ public class Pbkdf2Sha512PasswordHashProviderFactory implements PasswordHashProv
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PasswordHashProvider create(KeycloakSession session) {
|
public PasswordHashProvider create(KeycloakSession session) {
|
||||||
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS);
|
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS, getMaxPaddingLength());
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(Config.Scope config) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.junit.Test;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
import org.keycloak.credential.CredentialModel;
|
import org.keycloak.credential.CredentialModel;
|
||||||
|
import org.keycloak.credential.hash.PasswordHashProvider;
|
||||||
import org.keycloak.credential.hash.Pbkdf2PasswordHashProvider;
|
import org.keycloak.credential.hash.Pbkdf2PasswordHashProvider;
|
||||||
import org.keycloak.credential.hash.Pbkdf2PasswordHashProviderFactory;
|
import org.keycloak.credential.hash.Pbkdf2PasswordHashProviderFactory;
|
||||||
import org.keycloak.credential.hash.Pbkdf2Sha256PasswordHashProviderFactory;
|
import org.keycloak.credential.hash.Pbkdf2Sha256PasswordHashProviderFactory;
|
||||||
|
@ -46,6 +47,7 @@ import java.security.spec.KeySpec;
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,6 +170,7 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
||||||
Pbkdf2PasswordHashProvider specificKeySizeHashProvider = new Pbkdf2PasswordHashProvider(Pbkdf2Sha512PasswordHashProviderFactory.ID,
|
Pbkdf2PasswordHashProvider specificKeySizeHashProvider = new Pbkdf2PasswordHashProvider(Pbkdf2Sha512PasswordHashProviderFactory.ID,
|
||||||
Pbkdf2Sha512PasswordHashProviderFactory.PBKDF2_ALGORITHM,
|
Pbkdf2Sha512PasswordHashProviderFactory.PBKDF2_ALGORITHM,
|
||||||
Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS,
|
Pbkdf2Sha512PasswordHashProviderFactory.DEFAULT_ITERATIONS,
|
||||||
|
0,
|
||||||
256);
|
256);
|
||||||
String encodedPassword = specificKeySizeHashProvider.encode(password, -1);
|
String encodedPassword = specificKeySizeHashProvider.encode(password, -1);
|
||||||
|
|
||||||
|
@ -224,6 +227,32 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", 30000);
|
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA512", 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPbkdf2Sha256WithPadding() throws Exception {
|
||||||
|
setPasswordPolicy("hashAlgorithm(" + Pbkdf2Sha256PasswordHashProviderFactory.ID + ")");
|
||||||
|
|
||||||
|
int originalPaddingLength = configurePaddingForKeycloak(14);
|
||||||
|
try {
|
||||||
|
// Assert password created with padding enabled can be verified
|
||||||
|
String username1 = "test1-Pbkdf2Sha2562";
|
||||||
|
createUser(username1);
|
||||||
|
|
||||||
|
PasswordCredentialModel credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username1));
|
||||||
|
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", 27500);
|
||||||
|
|
||||||
|
// Now configure padding to bigger than 64. The verification without padding would fail as for longer padding than 64 characters, the hashes of the padded password and unpadded password would be different
|
||||||
|
configurePaddingForKeycloak(65);
|
||||||
|
String username2 = "test2-Pbkdf2Sha2562";
|
||||||
|
createUser(username2);
|
||||||
|
|
||||||
|
credential = PasswordCredentialModel.createFromCredentialModel(fetchCredentials(username2));
|
||||||
|
assertEncoded(credential, "password", credential.getPasswordSecretData().getSalt(), "PBKDF2WithHmacSHA256", 27500, false);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
configurePaddingForKeycloak(originalPaddingLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void createUser(String username) {
|
private void createUser(String username) {
|
||||||
ApiUtil.createUserAndResetPasswordWithAdminClient(adminClient.realm("test"), UserBuilder.create().username(username).build(), "password");
|
ApiUtil.createUserAndResetPasswordWithAdminClient(adminClient.realm("test"), UserBuilder.create().username(username).build(), "password");
|
||||||
|
@ -245,9 +274,26 @@ public class PasswordHashingTest extends AbstractTestRealmKeycloakTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertEncoded(PasswordCredentialModel credential, String password, byte[] salt, String algorithm, int iterations) throws Exception {
|
private void assertEncoded(PasswordCredentialModel credential, String password, byte[] salt, String algorithm, int iterations) throws Exception {
|
||||||
|
assertEncoded(credential, password, salt, algorithm, iterations, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertEncoded(PasswordCredentialModel credential, String password, byte[] salt, String algorithm, int iterations, boolean expectedSuccess) throws Exception {
|
||||||
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, 512);
|
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, 512);
|
||||||
byte[] key = SecretKeyFactory.getInstance(algorithm).generateSecret(spec).getEncoded();
|
byte[] key = SecretKeyFactory.getInstance(algorithm).generateSecret(spec).getEncoded();
|
||||||
assertEquals(Base64.encodeBytes(key), credential.getPasswordSecretData().getValue());
|
if (expectedSuccess) {
|
||||||
|
assertEquals(Base64.encodeBytes(key), credential.getPasswordSecretData().getValue());
|
||||||
|
} else {
|
||||||
|
assertNotEquals(Base64.encodeBytes(key), credential.getPasswordSecretData().getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int configurePaddingForKeycloak(int paddingLength) {
|
||||||
|
return testingClient.server("test").fetch(session -> {
|
||||||
|
Pbkdf2Sha256PasswordHashProviderFactory factory = (Pbkdf2Sha256PasswordHashProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(PasswordHashProvider.class, Pbkdf2Sha256PasswordHashProviderFactory.ID);
|
||||||
|
int origPaddingLength = factory.getMaxPaddingLength();
|
||||||
|
factory.setMaxPaddingLength(paddingLength);
|
||||||
|
return origPaddingLength;
|
||||||
|
}, Integer.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue