diff --git a/phase4-lib/src/main/java/com/helger/phase4/crypto/AS4CryptParams.java b/phase4-lib/src/main/java/com/helger/phase4/crypto/AS4CryptParams.java index cc516321b..0c94cc592 100644 --- a/phase4-lib/src/main/java/com/helger/phase4/crypto/AS4CryptParams.java +++ b/phase4-lib/src/main/java/com/helger/phase4/crypto/AS4CryptParams.java @@ -81,7 +81,8 @@ public class AS4CryptParams implements ICloneable // The alias into the WSS4J crypto config private String m_sAlias; // The session key provider - private ICryptoSessionKeyProvider m_aSessionKeyProvider = DEFAULT_SESSION_KEY_PROVIDER; + // null means: derive the key size from the encryption algorithm + private ICryptoSessionKeyProvider m_aSessionKeyProvider; private Provider m_aSecurityProviderEncrypt; private Provider m_aSecurityProviderDecrypt; private boolean m_bEncryptSymmetricSessionKey = DEFAULT_ENCRYPT_SYMMETRIC_SESSION_KEY; @@ -411,13 +412,31 @@ public final AS4CryptParams setAlias (@Nullable final String sAlias) } /** - * @return The session key provider to be used. Never null. + * @return The session key provider to be used. Never null. If no provider was set + * explicitly, the returned provider creates keys matching the key size of the configured + * encryption algorithm (e.g. 256 bits for AES-256-GCM), and falls back to + * {@link #DEFAULT_SESSION_KEY_PROVIDER} only if no encryption algorithm is set. * @since 2.1.2 */ @NonNull public final ICryptoSessionKeyProvider getSessionKeyProvider () { - return m_aSessionKeyProvider; + if (m_aSessionKeyProvider != null) + return m_aSessionKeyProvider; + if (m_eAlgorithmCrypt != null) + return ICryptoSessionKeyProvider.createRandomFor (m_eAlgorithmCrypt); + return DEFAULT_SESSION_KEY_PROVIDER; + } + + /** + * @return true if a session key provider was set explicitly via + * {@link #setSessionKeyProvider(ICryptoSessionKeyProvider)}, false if it is + * derived from the encryption algorithm. + * @since 4.8.0 + */ + public final boolean hasExplicitSessionKeyProvider () + { + return m_aSessionKeyProvider != null; } /** @@ -425,14 +444,14 @@ public final ICryptoSessionKeyProvider getSessionKeyProvider () * a null key. * * @param aSessionKeyProvider - * The session key provider to be used. May not be null. + * The session key provider to be used. May be null (since 4.8.0) to derive the + * session key from the encryption algorithm, which is the default. * @return this for chaining * @since 2.1.2 */ @NonNull - public final AS4CryptParams setSessionKeyProvider (@NonNull final ICryptoSessionKeyProvider aSessionKeyProvider) + public final AS4CryptParams setSessionKeyProvider (@Nullable final ICryptoSessionKeyProvider aSessionKeyProvider) { - ValueEnforcer.notNull (aSessionKeyProvider, "SessionKeyProvider"); m_aSessionKeyProvider = aSessionKeyProvider; return this; } 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 18e37dcda..46730003d 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 @@ -16,12 +16,18 @@ */ package com.helger.phase4.crypto; +import java.security.NoSuchAlgorithmException; +import java.util.function.Supplier; + +import javax.crypto.KeyGenerator; + import org.apache.wss4j.common.WSS4JConstants; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import com.helger.annotation.Nonempty; +import com.helger.annotation.Nonnegative; import com.helger.base.lang.EnumHelper; /** @@ -35,13 +41,37 @@ */ public enum ECryptoAlgorithmCrypt implements ICryptoAlgorithmCrypt { - 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); + CRYPT_3DES ("3des", "1.2.840.113549.3.7", WSS4JConstants.TRIPLE_DES, 192, () -> _createKeyGenerator ("DESede", 168)), + AES_128_CBC ("aes128-cbc", + "2.16.840.1.101.3.4.1.2", + WSS4JConstants.AES_128, + 128, + () -> _createKeyGenerator ("AES", 128)), + AES_128_GCM ("aes128-gcm", + "2.16.840.1.101.3.4.1.6", + WSS4JConstants.AES_128_GCM, + 128, + () -> _createKeyGenerator ("AES", 128)), + AES_192_CBC ("aes192-cbc", + "2.16.840.1.101.3.4.1.22", + WSS4JConstants.AES_192, + 192, + () -> _createKeyGenerator ("AES", 192)), + AES_192_GCM ("aes192-gcm", + "2.16.840.1.101.3.4.1.26", + WSS4JConstants.AES_192_GCM, + 192, + () -> _createKeyGenerator ("AES", 192)), + AES_256_CBC ("aes256-cbc", + "2.16.840.1.101.3.4.1.42", + WSS4JConstants.AES_256, + 256, + () -> _createKeyGenerator ("AES", 256)), + AES_256_GCM ("aes256-gcm", + "2.16.840.1.101.3.4.1.46", + WSS4JConstants.AES_256_GCM, + 256, + () -> _createKeyGenerator ("AES", 256)); /** Default encrypt algorithm */ public static final ECryptoAlgorithmCrypt ENCRYPTION_ALGORITHM_DEFAULT = AES_128_GCM; @@ -53,15 +83,43 @@ public enum ECryptoAlgorithmCrypt implements ICryptoAlgorithmCrypt private final String m_sID; private final String m_sOID; private final String m_sAlgorithmURI; + private final int m_nKeySizeBits; + private final Supplier m_aKeyGeneratorSupplier; private volatile ASN1ObjectIdentifier m_aOID; ECryptoAlgorithmCrypt (@NonNull @Nonempty final String sID, @NonNull @Nonempty final String sOID, - @NonNull @Nonempty final String sAlgorithmURI) + @NonNull @Nonempty final String sAlgorithmURI, + @Nonnegative final int nKeySizeBits, + @NonNull final Supplier aKeyGeneratorSupplier) { m_sID = sID; m_sOID = sOID; m_sAlgorithmURI = sAlgorithmURI; + m_nKeySizeBits = nKeySizeBits; + m_aKeyGeneratorSupplier = aKeyGeneratorSupplier; + } + + @NonNull + private static KeyGenerator _createKeyGenerator (@NonNull @Nonempty final String sJCEAlgorithm, + @Nonnegative final int nInitKeySizeBits) + { + try + { + // Plain JCE, so that this also works before WSS4J/XMLSec was initialized + final KeyGenerator ret = KeyGenerator.getInstance (sJCEAlgorithm); + ret.init (nInitKeySizeBits); + return ret; + } + catch (final NoSuchAlgorithmException ex) + { + throw new IllegalStateException ("Failed to create a " + + nInitKeySizeBits + + " bit KeyGenerator for '" + + sJCEAlgorithm + + "'", + ex); + } } @NonNull @@ -103,6 +161,30 @@ public String getAlgorithmURI () return m_sAlgorithmURI; } + /** + * @return The size of the encoded symmetric key this algorithm requires, in bits. E.g. 256 for + * AES-256-GCM, and 192 for 3DES (168 effective bits plus parity). + * @since 4.8.0 + */ + @Nonnegative + public int getKeySizeBits () + { + return m_nKeySizeBits; + } + + /** + * @return A new key generator that is initialized to create symmetric keys matching this + * algorithm. Never null. + * @throws IllegalStateException + * if the JCE algorithm is not available in the current runtime + * @since 4.8.0 + */ + @NonNull + public KeyGenerator createKeyGenerator () + { + return m_aKeyGeneratorSupplier.get (); + } + @Nullable public static ECryptoAlgorithmCrypt getFromIDOrNull (@Nullable final String sID) { diff --git a/phase4-lib/src/main/java/com/helger/phase4/crypto/ICryptoSessionKeyProvider.java b/phase4-lib/src/main/java/com/helger/phase4/crypto/ICryptoSessionKeyProvider.java index fa81e0537..878521b52 100644 --- a/phase4-lib/src/main/java/com/helger/phase4/crypto/ICryptoSessionKeyProvider.java +++ b/phase4-lib/src/main/java/com/helger/phase4/crypto/ICryptoSessionKeyProvider.java @@ -24,9 +24,12 @@ import org.apache.wss4j.common.util.KeyUtils; import org.jspecify.annotations.NonNull; +import com.helger.base.enforce.ValueEnforcer; + /** * Interface for a "session key" provider, that is used for encrypting documents. Default instances - * for AES-128 and AES-256 are provided for simplicity. + * for AES-128 and AES-256 are provided for simplicity, and {@link #createRandomFor(ECryptoAlgorithmCrypt)} + * creates a provider whose key size matches a given encryption algorithm. * * @author Philip Helger */ @@ -69,4 +72,20 @@ public interface ICryptoSessionKeyProvider throw new IllegalStateException ("Failed to create session key (AES-256)", ex); } }; + + /** + * Create a session key provider that creates random keys of exactly the size the provided + * encryption algorithm requires (e.g. 256 bits for AES-256-GCM). + * + * @param eAlgorithmCrypt + * The encryption algorithm the keys are used for. May not be null. + * @return A new session key provider. Never null. + * @since 4.8.0 + */ + @NonNull + static ICryptoSessionKeyProvider createRandomFor (@NonNull final ECryptoAlgorithmCrypt eAlgorithmCrypt) + { + ValueEnforcer.notNull (eAlgorithmCrypt, "AlgorithmCrypt"); + return () -> eAlgorithmCrypt.createKeyGenerator ().generateKey (); + } } diff --git a/phase4-lib/src/main/java/com/helger/phase4/messaging/crypto/AS4Encryptor.java b/phase4-lib/src/main/java/com/helger/phase4/messaging/crypto/AS4Encryptor.java index b7d929a87..4115585a2 100644 --- a/phase4-lib/src/main/java/com/helger/phase4/messaging/crypto/AS4Encryptor.java +++ b/phase4-lib/src/main/java/com/helger/phase4/messaging/crypto/AS4Encryptor.java @@ -39,6 +39,7 @@ import com.helger.phase4.attachment.WSS4JAttachmentCallbackHandler; import com.helger.phase4.config.AS4Configuration; import com.helger.phase4.crypto.AS4CryptParams; +import com.helger.phase4.crypto.ECryptoAlgorithmCrypt; import com.helger.phase4.crypto.ECryptoMode; import com.helger.phase4.crypto.IAS4CryptoFactory; import com.helger.phase4.logging.Phase4LoggerFactory; @@ -65,6 +66,31 @@ public final class AS4Encryptor private AS4Encryptor () {} + /** + * Make sure the session key has exactly the size the encryption algorithm requires. A key of the + * wrong size would otherwise be used silently, e.g. a 128 bit key for "aes256-gcm", which weakens + * the encryption and may be rejected by the receiver. + */ + static void checkSessionKeySize (@NonNull final AS4CryptParams aCryptParams, + @NonNull final SecretKey aSymmetricKey) + { + final ECryptoAlgorithmCrypt eAlgorithmCrypt = aCryptParams.getAlgorithmCrypt (); + final byte [] aEncoded = aSymmetricKey.getEncoded (); + if (eAlgorithmCrypt != null && aEncoded != null) + { + final int nActualBits = aEncoded.length * 8; + if (nActualBits != eAlgorithmCrypt.getKeySizeBits ()) + throw new IllegalStateException ("The session key has " + + nActualBits + + " bits, but encryption algorithm '" + + eAlgorithmCrypt.getID () + + "' requires " + + eAlgorithmCrypt.getKeySizeBits () + + " bits. Check the session key provider " + + aCryptParams.getSessionKeyProvider ()); + } + } + @NonNull private static WSSecEncrypt _createEncrypt (@NonNull final WSSecHeader aSecHeader, @NonNull final AS4CryptParams aCryptParams) @@ -173,6 +199,7 @@ private static Document _encryptSoapBodyPayload (@NonNull final IAS4CryptoFactor if (aSymmetricKey == null) throw new IllegalStateException ("Failed to create a symmetric session key from " + aCryptParams.getSessionKeyProvider ()); + checkSessionKeySize (aCryptParams, aSymmetricKey); return aBuilder.build (aCryptoFactoryCrypt.getCrypto (ECryptoMode.ENCRYPT_SIGN), aSymmetricKey); } @@ -286,6 +313,7 @@ private static AS4MimeMessage _encryptToMimeMessage (@NonNull final ESoapVersion if (aSymmetricKey == null) throw new IllegalStateException ("Failed to create a symmetric session key from " + aCryptParams.getSessionKeyProvider ()); + checkSessionKeySize (aCryptParams, aSymmetricKey); // Main sign and/or encrypt final Document aEncryptedDoc = aBuilder.build (aCryptoFactoryCrypt.getCrypto (ECryptoMode.ENCRYPT_SIGN), diff --git a/phase4-lib/src/test/java/com/helger/phase4/crypto/AS4CryptParamsTest.java b/phase4-lib/src/test/java/com/helger/phase4/crypto/AS4CryptParamsTest.java index 938f24737..78f4a40ba 100644 --- a/phase4-lib/src/test/java/com/helger/phase4/crypto/AS4CryptParamsTest.java +++ b/phase4-lib/src/test/java/com/helger/phase4/crypto/AS4CryptParamsTest.java @@ -195,4 +195,65 @@ public void testToStringWithoutKeyAgreement () // Key agreement fields should not appear when null assertFalse (sToString.contains ("KeyAgreementMethod")); } + + @Test + public void testSessionKeyMatchesAlgorithmByDefault () + { + for (final ECryptoAlgorithmCrypt e : ECryptoAlgorithmCrypt.values ()) + { + final AS4CryptParams aParams = new AS4CryptParams ().setAlgorithmCrypt (e); + assertFalse (aParams.hasExplicitSessionKeyProvider ()); + final int nBits = aParams.getSessionKeyProvider ().getSessionKey ().getEncoded ().length * 8; + assertEquals (e.getID (), e.getKeySizeBits (), nBits); + } + } + + @Test + public void testSessionKeyWithoutAlgorithmUsesDefault () + { + final AS4CryptParams aParams = new AS4CryptParams (); + assertSame (AS4CryptParams.DEFAULT_SESSION_KEY_PROVIDER, aParams.getSessionKeyProvider ()); + } + + @Test + public void testExplicitSessionKeyProviderWins () + { + final AS4CryptParams aParams = new AS4CryptParams ().setAlgorithmCrypt (ECryptoAlgorithmCrypt.AES_256_GCM) + .setSessionKeyProvider (ICryptoSessionKeyProvider.INSTANCE_RANDOM_AES_128); + assertTrue (aParams.hasExplicitSessionKeyProvider ()); + assertSame (ICryptoSessionKeyProvider.INSTANCE_RANDOM_AES_128, aParams.getSessionKeyProvider ()); + } + + @Test + public void testCloneKeepsDerivedSessionKeyProvider () + { + final AS4CryptParams aParams = new AS4CryptParams ().setAlgorithmCrypt (ECryptoAlgorithmCrypt.AES_256_CBC); + final AS4CryptParams aClone = aParams.getClone (); + assertFalse (aClone.hasExplicitSessionKeyProvider ()); + // Changing the algorithm of the clone must change the derived key size + aClone.setAlgorithmCrypt (ECryptoAlgorithmCrypt.AES_128_GCM); + assertEquals (16, aClone.getSessionKeyProvider ().getSessionKey ().getEncoded ().length); + assertEquals (32, aParams.getSessionKeyProvider ().getSessionKey ().getEncoded ().length); + } + + @Test + public void testSessionKeyProviderCanBeResetToDerived () + { + final AS4CryptParams aParams = new AS4CryptParams ().setAlgorithmCrypt (ECryptoAlgorithmCrypt.AES_256_GCM) + .setSessionKeyProvider (ICryptoSessionKeyProvider.INSTANCE_RANDOM_AES_128); + assertTrue (aParams.hasExplicitSessionKeyProvider ()); + aParams.setSessionKeyProvider (null); + assertFalse (aParams.hasExplicitSessionKeyProvider ()); + assertEquals (32, aParams.getSessionKeyProvider ().getSessionKey ().getEncoded ().length); + } + + @Test + public void testCloneKeepsExplicitSessionKeyProvider () + { + final AS4CryptParams aParams = new AS4CryptParams ().setAlgorithmCrypt (ECryptoAlgorithmCrypt.AES_256_GCM) + .setSessionKeyProvider (ICryptoSessionKeyProvider.INSTANCE_RANDOM_AES_256); + final AS4CryptParams aClone = aParams.getClone (); + assertTrue (aClone.hasExplicitSessionKeyProvider ()); + assertSame (ICryptoSessionKeyProvider.INSTANCE_RANDOM_AES_256, aClone.getSessionKeyProvider ()); + } } 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 aa26e20ce..a010fdca1 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 @@ -21,6 +21,8 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import javax.crypto.SecretKey; + import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.junit.Test; @@ -53,4 +55,16 @@ public void testBasic () assertSame (e, ECryptoAlgorithmCrypt.getFromIDOrThrow (e.getID ())); } } + + @Test + public void testKeyGeneratorMatchesKeySize () + { + for (final ECryptoAlgorithmCrypt e : ECryptoAlgorithmCrypt.values ()) + { + assertTrue (e.getID (), e.getKeySizeBits () > 0); + final SecretKey aKey = e.createKeyGenerator ().generateKey (); + assertNotNull (e.getID (), aKey); + assertEquals (e.getID (), e.getKeySizeBits (), aKey.getEncoded ().length * 8); + } + } } diff --git a/phase4-lib/src/test/java/com/helger/phase4/messaging/crypto/AS4EncryptorTest.java b/phase4-lib/src/test/java/com/helger/phase4/messaging/crypto/AS4EncryptorTest.java new file mode 100644 index 000000000..5274c8d40 --- /dev/null +++ b/phase4-lib/src/test/java/com/helger/phase4/messaging/crypto/AS4EncryptorTest.java @@ -0,0 +1,58 @@ +/* + * 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.messaging.crypto; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import javax.crypto.spec.SecretKeySpec; + +import org.junit.Test; + +import com.helger.phase4.crypto.AS4CryptParams; +import com.helger.phase4.crypto.ECryptoAlgorithmCrypt; + +/** + * Test class for class {@link AS4Encryptor}. + * + * @author Dino Patti + */ +public final class AS4EncryptorTest +{ + @Test + public void testSessionKeySizeMatches () + { + final AS4CryptParams aParams = new AS4CryptParams ().setAlgorithmCrypt (ECryptoAlgorithmCrypt.AES_256_GCM); + AS4Encryptor.checkSessionKeySize (aParams, new SecretKeySpec (new byte [32], "AES")); + } + + @Test + public void testSessionKeyTooShortIsRejected () + { + final AS4CryptParams aParams = new AS4CryptParams ().setAlgorithmCrypt (ECryptoAlgorithmCrypt.AES_256_GCM); + try + { + AS4Encryptor.checkSessionKeySize (aParams, new SecretKeySpec (new byte [16], "AES")); + fail (); + } + catch (final IllegalStateException ex) + { + assertTrue (ex.getMessage (), ex.getMessage ().contains ("128 bits")); + assertTrue (ex.getMessage (), ex.getMessage ().contains ("aes256-gcm")); + } + } +}