From bfdda5f7131240c56c7bf3bfc35f1198ae774284 Mon Sep 17 00:00:00 2001 From: GT Date: Tue, 11 Aug 2026 10:05:38 +0200 Subject: [PATCH] Make Bouncy Castle optional in phase4-lib Keep standards-defined encryption OIDs as provider-neutral strings so phase4-lib does not require ph-bc merely for algorithm metadata. Preserve the BC-typed OID method lazily for compatibility and keep ph-bc explicit in modules with real BC usage. --- phase4-bdew-client/pom.xml | 5 ++ phase4-edelivery2-client/pom.xml | 5 ++ phase4-lib/pom.xml | 3 + .../phase4/crypto/ECryptoAlgorithmCrypt.java | 43 ++++++--- .../phase4/crypto/ICryptoAlgorithmCrypt.java | 22 +++++ .../crypto/ECryptoAlgorithmCryptTest.java | 5 ++ .../ECryptoAlgorithmCryptWithoutBCProbe.java | 33 +++++++ .../ECryptoAlgorithmCryptWithoutBCTest.java | 89 +++++++++++++++++++ phase4-profile-bdew/pom.xml | 4 + 9 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptWithoutBCProbe.java create mode 100644 phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptWithoutBCTest.java diff --git a/phase4-bdew-client/pom.xml b/phase4-bdew-client/pom.xml index 0f86c88f4..9d58c3370 100644 --- a/phase4-bdew-client/pom.xml +++ b/phase4-bdew-client/pom.xml @@ -85,6 +85,11 @@ ph-unittest-support-ext test + + com.helger.commons + ph-bc + test + org.bouncycastle bctls-jdk18on diff --git a/phase4-edelivery2-client/pom.xml b/phase4-edelivery2-client/pom.xml index 125cc8d49..57ecd157d 100644 --- a/phase4-edelivery2-client/pom.xml +++ b/phase4-edelivery2-client/pom.xml @@ -95,6 +95,11 @@ ph-unittest-support-ext test + + com.helger.commons + ph-bc + test + diff --git a/phase4-lib/pom.xml b/phase4-lib/pom.xml index 0c239e24c..33223b379 100644 --- a/phase4-lib/pom.xml +++ b/phase4-lib/pom.xml @@ -63,6 +63,9 @@ com.helger.commons ph-bc + + true com.helger.commons diff --git a/phase4-lib/src/main/java/com/helger/phase4/crypto/ECryptoAlgorithmCrypt.java b/phase4-lib/src/main/java/com/helger/phase4/crypto/ECryptoAlgorithmCrypt.java index c8c418564..da8884253 100644 --- a/phase4-lib/src/main/java/com/helger/phase4/crypto/ECryptoAlgorithmCrypt.java +++ b/phase4-lib/src/main/java/com/helger/phase4/crypto/ECryptoAlgorithmCrypt.java @@ -18,7 +18,6 @@ import org.apache.wss4j.common.WSS4JConstants; import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.cms.CMSAlgorithm; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -29,16 +28,19 @@ * Enumeration with all message encryption algorithms supported. * * @author Philip Helger + * @apiNote Direct use of this enum, except for the deprecated {@link #getOID()} method, does not + * require Bouncy Castle. Reflection and AOT tools that eagerly resolve all method + * descriptors still require it for binary compatibility with that method. */ public enum ECryptoAlgorithmCrypt implements ICryptoAlgorithmCrypt { - CRYPT_3DES ("3des", CMSAlgorithm.DES_EDE3_CBC, WSS4JConstants.TRIPLE_DES), - AES_128_CBC ("aes128-cbc", CMSAlgorithm.AES128_CBC, WSS4JConstants.AES_128), - AES_128_GCM ("aes128-gcm", CMSAlgorithm.AES128_GCM, WSS4JConstants.AES_128_GCM), - AES_192_CBC ("aes192-cbc", CMSAlgorithm.AES192_CBC, WSS4JConstants.AES_192), - AES_192_GCM ("aes192-gcm", CMSAlgorithm.AES192_GCM, WSS4JConstants.AES_192_GCM), - AES_256_CBC ("aes256-cbc", CMSAlgorithm.AES256_CBC, WSS4JConstants.AES_256), - AES_256_GCM ("aes256-gcm", CMSAlgorithm.AES256_GCM, WSS4JConstants.AES_256_GCM); + CRYPT_3DES ("3des", "1.2.840.113549.3.7", WSS4JConstants.TRIPLE_DES), + AES_128_CBC ("aes128-cbc", "2.16.840.1.101.3.4.1.2", WSS4JConstants.AES_128), + AES_128_GCM ("aes128-gcm", "2.16.840.1.101.3.4.1.6", WSS4JConstants.AES_128_GCM), + AES_192_CBC ("aes192-cbc", "2.16.840.1.101.3.4.1.22", WSS4JConstants.AES_192), + AES_192_GCM ("aes192-gcm", "2.16.840.1.101.3.4.1.26", WSS4JConstants.AES_192_GCM), + AES_256_CBC ("aes256-cbc", "2.16.840.1.101.3.4.1.42", WSS4JConstants.AES_256), + AES_256_GCM ("aes256-gcm", "2.16.840.1.101.3.4.1.46", WSS4JConstants.AES_256_GCM); /** Default encrypt algorithm */ public static final ECryptoAlgorithmCrypt ENCRYPTION_ALGORITHM_DEFAULT = AES_128_GCM; @@ -48,15 +50,16 @@ public enum ECryptoAlgorithmCrypt implements ICryptoAlgorithmCrypt public static final ECryptoAlgorithmCrypt ENCRPYTION_ALGORITHM_DEFAULT = ENCRYPTION_ALGORITHM_DEFAULT; private final String m_sID; - private final ASN1ObjectIdentifier m_aOID; + private final String m_sOID; private final String m_sAlgorithmURI; + private volatile ASN1ObjectIdentifier m_aOID; ECryptoAlgorithmCrypt (@NonNull @Nonempty final String sID, - @NonNull final ASN1ObjectIdentifier aOID, + @NonNull @Nonempty final String sOID, @NonNull @Nonempty final String sAlgorithmURI) { m_sID = sID; - m_aOID = aOID; + m_sOID = sOID; m_sAlgorithmURI = sAlgorithmURI; } @@ -68,9 +71,25 @@ public String getID () } @NonNull + @Nonempty + public String getOIDString () + { + return m_sOID; + } + + @NonNull + @Deprecated (since = "4.6.0") public ASN1ObjectIdentifier getOID () { - return m_aOID; + ASN1ObjectIdentifier ret = m_aOID; + if (ret == null) + synchronized (this) + { + ret = m_aOID; + if (ret == null) + m_aOID = ret = new ASN1ObjectIdentifier (m_sOID); + } + return ret; } /** diff --git a/phase4-lib/src/main/java/com/helger/phase4/crypto/ICryptoAlgorithmCrypt.java b/phase4-lib/src/main/java/com/helger/phase4/crypto/ICryptoAlgorithmCrypt.java index bf85c3138..93de2f3fa 100644 --- a/phase4-lib/src/main/java/com/helger/phase4/crypto/ICryptoAlgorithmCrypt.java +++ b/phase4-lib/src/main/java/com/helger/phase4/crypto/ICryptoAlgorithmCrypt.java @@ -27,6 +27,11 @@ * * @author Philip Helger * @since v1.4.4 + * @apiNote Implementations that override {@link #getOIDString()} can support direct use without + * Bouncy Castle. This interface nevertheless retains the deprecated {@link #getOID()} + * method for binary compatibility. Reflection and AOT tools that eagerly resolve every + * method descriptor therefore still require Bouncy Castle until that method can be + * removed in a future major release. */ public interface ICryptoAlgorithmCrypt extends IHasID { @@ -37,10 +42,27 @@ public interface ICryptoAlgorithmCrypt extends IHasID @Nonempty String getID (); + /** + * @return The OID of the algorithm in dot-decimal notation. + * @implSpec Implementations should override this method to make direct invocation independent + * of Bouncy Castle. The default implementation delegates to {@link #getOID()} for + * binary compatibility with existing implementations. + * @since 4.6.0 + */ + @NonNull + @Nonempty + default String getOIDString () + { + return getOID ().getId (); + } + /** * @return The OID of the algorithm to be used by the Security Provider. + * @deprecated Use {@link #getOIDString()} instead. This compatibility method requires Bouncy + * Castle to be present at runtime. */ @NonNull + @Deprecated (since = "4.6.0") ASN1ObjectIdentifier getOID (); /** diff --git a/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptTest.java b/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptTest.java index f72804981..3abb87cf7 100644 --- a/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptTest.java +++ b/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptTest.java @@ -16,6 +16,7 @@ */ package com.helger.phase4.crypto; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -32,12 +33,16 @@ public final class ECryptoAlgorithmCryptTest { @Test + @SuppressWarnings ("deprecation") public void testBasic () { for (final ECryptoAlgorithmCrypt e : ECryptoAlgorithmCrypt.values ()) { assertTrue (StringHelper.isNotEmpty (e.getID ())); + assertTrue (StringHelper.isNotEmpty (e.getOIDString ())); assertNotNull (e.getOID ()); + assertEquals (e.getOIDString (), e.getOID ().getId ()); + assertSame (e.getOID (), e.getOID ()); assertTrue (StringHelper.isNotEmpty (e.getAlgorithmURI ())); assertSame (e, ECryptoAlgorithmCrypt.getFromIDOrNull (e.getID ())); assertSame (e, ECryptoAlgorithmCrypt.getFromIDOrDefault (e.getID (), null)); diff --git a/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptWithoutBCProbe.java b/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptWithoutBCProbe.java new file mode 100644 index 000000000..3115ca799 --- /dev/null +++ b/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptWithoutBCProbe.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015-2026 Philip Helger (www.helger.com) + * philip[at]helger[dot]com + * + * 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 com.helger.phase4.crypto; + +/** Invoked through an isolated class loader by {@link ECryptoAlgorithmCryptWithoutBCTest}. */ +public final class ECryptoAlgorithmCryptWithoutBCProbe +{ + private ECryptoAlgorithmCryptWithoutBCProbe () + {} + + public static String [] getOIDStrings () + { + final ECryptoAlgorithmCrypt [] aValues = ECryptoAlgorithmCrypt.values (); + final String [] ret = new String [aValues.length]; + for (int i = 0; i < aValues.length; ++i) + ret[i] = aValues[i].getOIDString (); + return ret; + } +} diff --git a/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptWithoutBCTest.java b/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptWithoutBCTest.java new file mode 100644 index 000000000..1d02555c7 --- /dev/null +++ b/phase4-lib/src/test/java/com/helger/phase4/crypto/ECryptoAlgorithmCryptWithoutBCTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2015-2026 Philip Helger (www.helger.com) + * philip[at]helger[dot]com + * + * 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 com.helger.phase4.crypto; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.Test; + +/** + * Verifies direct enum initialization and provider-neutral OID access without Bouncy Castle. This + * deliberately does not reflect over all enum methods because the retained legacy method + * descriptor still references a Bouncy Castle type. + */ +public final class ECryptoAlgorithmCryptWithoutBCTest +{ + private static final String ENUM_CLASS = ECryptoAlgorithmCrypt.class.getName (); + private static final String INTERFACE_CLASS = ICryptoAlgorithmCrypt.class.getName (); + private static final String PROBE_CLASS = ECryptoAlgorithmCryptWithoutBCProbe.class.getName (); + + @Test + public void testDirectEnumUseWithoutBC () throws Exception + { + final URL [] aURLs = { new File ("target/classes").toURI ().toURL (), + new File ("target/test-classes").toURI ().toURL () }; + try (final URLClassLoader aCL = new URLClassLoader (aURLs, getClass ().getClassLoader ()) + { + @Override + protected Class loadClass (final String sName, final boolean bResolve) throws ClassNotFoundException + { + if (sName.startsWith ("org.bouncycastle.")) + throw new ClassNotFoundException ("Bouncy Castle deliberately hidden from test class loader"); + + if (sName.equals (ENUM_CLASS) || sName.equals (INTERFACE_CLASS) || sName.equals (PROBE_CLASS)) + synchronized (getClassLoadingLock (sName)) + { + Class ret = findLoadedClass (sName); + if (ret == null) + ret = findClass (sName); + if (bResolve) + resolveClass (ret); + return ret; + } + + return super.loadClass (sName, bResolve); + } + }) + { + try + { + aCL.loadClass ("org.bouncycastle.asn1.ASN1ObjectIdentifier"); + fail ("Bouncy Castle must not be visible to the isolated class loader"); + } + catch (final ClassNotFoundException ex) + { + // Expected + } + + final Class aProbeClass = Class.forName (PROBE_CLASS, true, aCL); + final String [] aActual = (String []) aProbeClass.getMethod ("getOIDStrings").invoke (null); + assertArrayEquals (new String [] { "1.2.840.113549.3.7", + "2.16.840.1.101.3.4.1.2", + "2.16.840.1.101.3.4.1.6", + "2.16.840.1.101.3.4.1.22", + "2.16.840.1.101.3.4.1.26", + "2.16.840.1.101.3.4.1.42", + "2.16.840.1.101.3.4.1.46" }, + aActual); + } + } +} diff --git a/phase4-profile-bdew/pom.xml b/phase4-profile-bdew/pom.xml index ebfc615ec..3bbf805e5 100644 --- a/phase4-profile-bdew/pom.xml +++ b/phase4-profile-bdew/pom.xml @@ -49,6 +49,10 @@ com.helger.phase4 phase4-lib + + com.helger.commons + ph-bc + jakarta.servlet jakarta.servlet-api