Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
Expand All @@ -27,10 +28,18 @@ public class CryptoIntegration {
private static volatile CryptoProvider cryptoProvider;

public static void init(ClassLoader classLoader) {
init(() -> detectProvider(classLoader));
}

public static void init(ClassLoader classLoader, String providerName, FipsMode fipsMode) {
init(() -> detectProvider(classLoader, providerName, fipsMode));
}

private static void init(Supplier<CryptoProvider> providerSupplier) {
if (cryptoProvider == null) {
synchronized (lock) {
if (cryptoProvider == null) {
cryptoProvider = detectProvider(classLoader);
cryptoProvider = providerSupplier.get();
logger.debugv("java security provider: {0}", BouncyIntegration.PROVIDER);

}
Expand All @@ -43,6 +52,27 @@ public static void init(ClassLoader classLoader) {
}
}

static CryptoProvider detectProvider(Iterable<CryptoProviderFactory> factories, String providerName, FipsMode fipsMode) {
List<CryptoProviderFactory> matchingFactories = StreamSupport.stream(factories.spliterator(), false)
.filter(factory -> providerName.equals(factory.getName()))
.collect(Collectors.toList());

if (matchingFactories.size() != 1) {
throw new IllegalStateException("Expected one crypto provider named '" + providerName + "', found " + matchingFactories.size());
}

CryptoProvider provider = matchingFactories.get(0).create(fipsMode);
if (provider == null) {
throw new IllegalStateException("Crypto provider factory '" + providerName + "' returned null");
}
logger.debugf("Detected crypto provider: %s", provider.getClass().getName());
return provider;
}

private static CryptoProvider detectProvider(ClassLoader classLoader, String providerName, FipsMode fipsMode) {
return detectProvider(ServiceLoader.load(CryptoProviderFactory.class, classLoader), providerName, fipsMode);
}

public static boolean isInitialised() {
return cryptoProvider != null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2026 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.crypto;

public interface CryptoProviderFactory {

String getName();

CryptoProvider create(FipsMode fipsMode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2026 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.crypto;

public enum FipsProvider {
AUTO("auto"),
BOUNCY_CASTLE("bouncycastle"),
GLASSLESS("glassless");

private static final String BOUNCY_CASTLE_FIPS_PROVIDER =
"org/bouncycastle/jcajce/provider/BouncyCastleFipsProvider.class";

private final String optionName;

FipsProvider(String optionName) {
this.optionName = optionName;
}

public static FipsProvider valueOfOption(String name) {
for (FipsProvider provider : values()) {
if (provider.optionName.equals(name)) {
return provider;
}
}
throw new IllegalArgumentException("Unknown FIPS provider: " + name);
}

public FipsProvider resolve(ClassLoader classLoader) {
if (this != AUTO) {
return this;
}
return classLoader.getResource(BOUNCY_CASTLE_FIPS_PROVIDER) == null ? GLASSLESS : BOUNCY_CASTLE;
}

@Override
public String toString() {
return optionName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2026 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.crypto;

import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class CryptoProviderFactoryTest {

@Test
public void shouldSelectFactoryByNameAndPassFipsMode() {
CryptoProvider expected = provider();
AtomicReference<FipsMode> selectedMode = new AtomicReference<>();
List<CryptoProviderFactory> factories = Arrays.asList(
factory("default", provider(), new AtomicReference<>()),
factory("glassless", expected, selectedMode));

CryptoProvider actual = CryptoIntegration.detectProvider(factories, "glassless", FipsMode.STRICT);

assertSame(expected, actual);
assertEquals(FipsMode.STRICT, selectedMode.get());
}

@Test
public void shouldRejectMissingFactory() {
IllegalStateException cause = assertThrows(IllegalStateException.class,
() -> CryptoIntegration.detectProvider(Collections.emptyList(), "glassless", FipsMode.STRICT));

assertEquals("Expected one crypto provider named 'glassless', found 0", cause.getMessage());
}

@Test
public void shouldRejectDuplicateFactories() {
List<CryptoProviderFactory> factories = Arrays.asList(
factory("glassless", provider(), new AtomicReference<>()),
factory("glassless", provider(), new AtomicReference<>()));

IllegalStateException cause = assertThrows(IllegalStateException.class,
() -> CryptoIntegration.detectProvider(factories, "glassless", FipsMode.STRICT));

assertEquals("Expected one crypto provider named 'glassless', found 2", cause.getMessage());
}

@Test
public void shouldRejectNullProvider() {
IllegalStateException cause = assertThrows(IllegalStateException.class,
() -> CryptoIntegration.detectProvider(
Collections.singletonList(factory("glassless", null, new AtomicReference<>())), "glassless", FipsMode.STRICT));

assertEquals("Crypto provider factory 'glassless' returned null", cause.getMessage());
}

private static CryptoProviderFactory factory(String name, CryptoProvider provider, AtomicReference<FipsMode> selectedMode) {
return new CryptoProviderFactory() {
@Override
public String getName() {
return name;
}

@Override
public CryptoProvider create(FipsMode fipsMode) {
selectedMode.set(fipsMode);
return provider;
}
};
}

private static CryptoProvider provider() {
return (CryptoProvider) Proxy.newProxyInstance(CryptoProvider.class.getClassLoader(),
new Class<?>[] { CryptoProvider.class }, (proxy, method, args) -> null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2026 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.crypto;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class FipsProviderTest {

@Test
public void shouldResolveProviderOptions() {
assertEquals(FipsProvider.AUTO, FipsProvider.valueOfOption("auto"));
assertEquals(FipsProvider.BOUNCY_CASTLE, FipsProvider.valueOfOption("bouncycastle"));
assertEquals(FipsProvider.GLASSLESS, FipsProvider.valueOfOption("glassless"));
assertEquals("auto", FipsProvider.AUTO.toString());
assertEquals("bouncycastle", FipsProvider.BOUNCY_CASTLE.toString());
assertEquals("glassless", FipsProvider.GLASSLESS.toString());
assertThrows(IllegalArgumentException.class, () -> FipsProvider.valueOfOption("unknown"));
}

@Test
public void shouldSelectGlasslessWhenBouncyCastleIsUnavailable() {
assertEquals(FipsProvider.GLASSLESS, FipsProvider.AUTO.resolve(new ClassLoader(null) {
}));
}

@Test
public void shouldSelectBouncyCastleWhenItsProviderClassIsAvailable() {
ClassLoader classLoader = new ClassLoader(null) {
@Override
public java.net.URL getResource(String name) {
return "org/bouncycastle/jcajce/provider/BouncyCastleFipsProvider.class".equals(name)
? FipsProviderTest.class.getResource("FipsProviderTest.class")
: null;
}
};

assertEquals(FipsProvider.BOUNCY_CASTLE, FipsProvider.AUTO.resolve(classLoader));
}

@Test
public void shouldPreserveExplicitProviderSelection() {
ClassLoader classLoader = new ClassLoader(null) {
@Override
public java.net.URL getResource(String name) {
return FipsProviderTest.class.getResource("FipsProviderTest.class");
}
};

assertEquals(FipsProvider.GLASSLESS, FipsProvider.GLASSLESS.resolve(classLoader));
assertEquals(FipsProvider.BOUNCY_CASTLE, FipsProvider.BOUNCY_CASTLE.resolve(new ClassLoader(null) {
}));
}

@Test
public void shouldKeepExistingProviderClassNames() {
assertEquals("org.keycloak.crypto.fips.FIPS1402Provider", FipsMode.NON_STRICT.getProviderClassName());
assertEquals("org.keycloak.crypto.fips.Fips1402StrictCryptoProvider", FipsMode.STRICT.getProviderClassName());
assertEquals("org.keycloak.crypto.def.DefaultCryptoProvider", FipsMode.DISABLED.getProviderClassName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2026 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.crypto.def;

import org.keycloak.common.crypto.CryptoProvider;
import org.keycloak.common.crypto.CryptoProviderFactory;
import org.keycloak.common.crypto.FipsMode;

public class DefaultCryptoProviderFactory implements CryptoProviderFactory {

@Override
public String getName() {
return "default";
}

@Override
public CryptoProvider create(FipsMode fipsMode) {
if (fipsMode.isFipsEnabled()) {
throw new IllegalArgumentException("The default crypto provider does not support FIPS mode");
}
return new DefaultCryptoProvider();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# Copyright 2026 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.
#

org.keycloak.crypto.def.DefaultCryptoProviderFactory
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import org.keycloak.jose.jwe.JWEHeader;
import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder;
import org.keycloak.jose.jwe.JWEKeyStorage;
import org.keycloak.jose.jwe.JWEKeyStorage.KeyUse;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;

Expand All @@ -34,15 +33,15 @@ public class AesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider {
@Override
public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception {
Cipher cipher = Cipher.getInstance("AESWrap_128");
cipher.init(Cipher.UNWRAP_MODE, encryptionKey);
return cipher.unwrap(encodedCek, "AES", Cipher.SECRET_KEY).getEncoded();
cipher.init(Cipher.DECRYPT_MODE, encryptionKey);
return cipher.doFinal(encodedCek);
}

@Override
public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception {
Cipher cipher = Cipher.getInstance("AESWrap_128");
cipher.init(Cipher.WRAP_MODE, encryptionKey);
return cipher.wrap(keyStorage.getCEKKey(KeyUse.ENCRYPTION, false));
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey);
return cipher.doFinal(keyStorage.getCekBytes());
}


Expand Down
Loading
Loading