From 7b3dd3460b84e550e2b41d2a4cc8b2222c79d14a Mon Sep 17 00:00:00 2001 From: Dino Patti Date: Wed, 30 Sep 2026 09:54:04 +0200 Subject: [PATCH 1/2] Session key size follows the encryption algorithm AS4CryptParams used a 128-bit session key by default even when an AES-256 algorithm was configured, so messages labelled aes256-cbc/aes256-gcm were encrypted with 128-bit keys. The default provider is now derived from the configured algorithm (an explicitly set provider still wins), and AS4Encryptor rejects a session key whose size does not match the algorithm. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../helger/phase4/crypto/AS4CryptParams.java | 28 +++++++-- .../phase4/crypto/ECryptoAlgorithmCrypt.java | 31 +++++++--- .../crypto/ICryptoSessionKeyProvider.java | 41 ++++++++++++- .../phase4/messaging/crypto/AS4Encryptor.java | 28 +++++++++ .../phase4/crypto/AS4CryptParamsTest.java | 40 +++++++++++++ .../messaging/crypto/AS4EncryptorTest.java | 58 +++++++++++++++++++ 6 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 phase4-lib/src/test/java/com/helger/phase4/messaging/crypto/AS4EncryptorTest.java 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..3910ac42f 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.7.1 + */ + public final boolean hasExplicitSessionKeyProvider () + { + return m_aSessionKeyProvider != null; } /** @@ -590,11 +609,12 @@ public void cloneTo (@NonNull final AS4CryptParams aTarget) .setKeyWrapAlgorithm (m_eKeyWrapAlgorithm) .setCertificate (m_aCert) .setAlias (m_sAlias) - .setSessionKeyProvider (m_aSessionKeyProvider) .setSecurityProviderEncrypt (m_aSecurityProviderEncrypt) .setSecurityProviderDecrypt (m_aSecurityProviderDecrypt) .setEncryptSymmetricSessionKey (m_bEncryptSymmetricSessionKey) .setWSSecEncryptCustomizer (m_aWSSecEncryptCustomizer); + // Copy the field directly, so that "derived from the algorithm" survives the clone + aTarget.m_aSessionKeyProvider = m_aSessionKeyProvider; } @NonNull 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..4d9aab5a6 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 @@ -22,6 +22,7 @@ import org.jspecify.annotations.Nullable; import com.helger.annotation.Nonempty; +import com.helger.annotation.Nonnegative; import com.helger.base.lang.EnumHelper; /** @@ -35,13 +36,13 @@ */ 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), + AES_128_CBC ("aes128-cbc", "2.16.840.1.101.3.4.1.2", WSS4JConstants.AES_128, 128), + AES_128_GCM ("aes128-gcm", "2.16.840.1.101.3.4.1.6", WSS4JConstants.AES_128_GCM, 128), + AES_192_CBC ("aes192-cbc", "2.16.840.1.101.3.4.1.22", WSS4JConstants.AES_192, 192), + AES_192_GCM ("aes192-gcm", "2.16.840.1.101.3.4.1.26", WSS4JConstants.AES_192_GCM, 192), + AES_256_CBC ("aes256-cbc", "2.16.840.1.101.3.4.1.42", WSS4JConstants.AES_256, 256), + AES_256_GCM ("aes256-gcm", "2.16.840.1.101.3.4.1.46", WSS4JConstants.AES_256_GCM, 256); /** Default encrypt algorithm */ public static final ECryptoAlgorithmCrypt ENCRYPTION_ALGORITHM_DEFAULT = AES_128_GCM; @@ -53,15 +54,18 @@ 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 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) { m_sID = sID; m_sOID = sOID; m_sAlgorithmURI = sAlgorithmURI; + m_nKeySizeBits = nKeySizeBits; } @NonNull @@ -103,6 +107,17 @@ public String getAlgorithmURI () return m_sAlgorithmURI; } + /** + * @return The size of the symmetric key this algorithm requires, in bits. E.g. 256 for + * AES-256-GCM. + * @since 4.7.1 + */ + @Nonnegative + public int getKeySizeBits () + { + return m_nKeySizeBits; + } + @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..142e699c7 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 @@ -16,6 +16,8 @@ */ package com.helger.phase4.crypto; +import java.security.NoSuchAlgorithmException; + import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -24,9 +26,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 +74,38 @@ 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.7.1 + */ + @NonNull + static ICryptoSessionKeyProvider createRandomFor (@NonNull final ECryptoAlgorithmCrypt eAlgorithmCrypt) + { + ValueEnforcer.notNull (eAlgorithmCrypt, "AlgorithmCrypt"); + return () -> { + try + { + // Plain JCE, so that this also works before WSS4J/XMLSec was initialized + final KeyGenerator aKeyGen; + if (eAlgorithmCrypt == ECryptoAlgorithmCrypt.CRYPT_3DES) + aKeyGen = KeyGenerator.getInstance ("DESede"); + else + { + aKeyGen = KeyGenerator.getInstance ("AES"); + aKeyGen.init (eAlgorithmCrypt.getKeySizeBits ()); + } + return aKeyGen.generateKey (); + } + catch (final NoSuchAlgorithmException ex) + { + throw new IllegalStateException ("Failed to create session key (" + eAlgorithmCrypt.getID () + ")", ex); + } + }; + } } 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..77d1c9e47 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,44 @@ 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); + } } 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")); + } + } +} From 4e9b9f7f719baafc6865f9dfaadb0a9ee732a387 Mon Sep 17 00:00:00 2001 From: Dino Patti Date: Wed, 30 Sep 2026 10:30:02 +0200 Subject: [PATCH 2/2] Review: key generator per algorithm, nullable session key provider - ECryptoAlgorithmCrypt gets a KeyGenerator supplier per constant and createKeyGenerator(), so createRandomFor needs no special case for 3DES - AS4CryptParams.setSessionKeyProvider accepts null (derive from the encryption algorithm), so cloneTo goes through the setter again - @since 4.8.0 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../helger/phase4/crypto/AS4CryptParams.java | 11 ++- .../phase4/crypto/ECryptoAlgorithmCrypt.java | 89 ++++++++++++++++--- .../crypto/ICryptoSessionKeyProvider.java | 24 +---- .../phase4/crypto/AS4CryptParamsTest.java | 21 +++++ .../crypto/ECryptoAlgorithmCryptTest.java | 14 +++ 5 files changed, 120 insertions(+), 39 deletions(-) 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 3910ac42f..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 @@ -432,7 +432,7 @@ public final ICryptoSessionKeyProvider getSessionKeyProvider () * @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.7.1 + * @since 4.8.0 */ public final boolean hasExplicitSessionKeyProvider () { @@ -444,14 +444,14 @@ public final boolean hasExplicitSessionKeyProvider () * 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; } @@ -609,12 +609,11 @@ public void cloneTo (@NonNull final AS4CryptParams aTarget) .setKeyWrapAlgorithm (m_eKeyWrapAlgorithm) .setCertificate (m_aCert) .setAlias (m_sAlias) + .setSessionKeyProvider (m_aSessionKeyProvider) .setSecurityProviderEncrypt (m_aSecurityProviderEncrypt) .setSecurityProviderDecrypt (m_aSecurityProviderDecrypt) .setEncryptSymmetricSessionKey (m_bEncryptSymmetricSessionKey) .setWSSecEncryptCustomizer (m_aWSSecEncryptCustomizer); - // Copy the field directly, so that "derived from the algorithm" survives the clone - aTarget.m_aSessionKeyProvider = m_aSessionKeyProvider; } @NonNull 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 4d9aab5a6..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,6 +16,11 @@ */ 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; @@ -36,13 +41,37 @@ */ public enum ECryptoAlgorithmCrypt implements ICryptoAlgorithmCrypt { - CRYPT_3DES ("3des", "1.2.840.113549.3.7", WSS4JConstants.TRIPLE_DES, 192), - AES_128_CBC ("aes128-cbc", "2.16.840.1.101.3.4.1.2", WSS4JConstants.AES_128, 128), - AES_128_GCM ("aes128-gcm", "2.16.840.1.101.3.4.1.6", WSS4JConstants.AES_128_GCM, 128), - AES_192_CBC ("aes192-cbc", "2.16.840.1.101.3.4.1.22", WSS4JConstants.AES_192, 192), - AES_192_GCM ("aes192-gcm", "2.16.840.1.101.3.4.1.26", WSS4JConstants.AES_192_GCM, 192), - AES_256_CBC ("aes256-cbc", "2.16.840.1.101.3.4.1.42", WSS4JConstants.AES_256, 256), - AES_256_GCM ("aes256-gcm", "2.16.840.1.101.3.4.1.46", WSS4JConstants.AES_256_GCM, 256); + 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; @@ -55,17 +84,42 @@ public enum ECryptoAlgorithmCrypt implements ICryptoAlgorithmCrypt 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, - @Nonnegative final int nKeySizeBits) + @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 @@ -108,9 +162,9 @@ public String getAlgorithmURI () } /** - * @return The size of the symmetric key this algorithm requires, in bits. E.g. 256 for - * AES-256-GCM. - * @since 4.7.1 + * @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 () @@ -118,6 +172,19 @@ 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 142e699c7..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 @@ -16,8 +16,6 @@ */ package com.helger.phase4.crypto; -import java.security.NoSuchAlgorithmException; - import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -82,30 +80,12 @@ public interface ICryptoSessionKeyProvider * @param eAlgorithmCrypt * The encryption algorithm the keys are used for. May not be null. * @return A new session key provider. Never null. - * @since 4.7.1 + * @since 4.8.0 */ @NonNull static ICryptoSessionKeyProvider createRandomFor (@NonNull final ECryptoAlgorithmCrypt eAlgorithmCrypt) { ValueEnforcer.notNull (eAlgorithmCrypt, "AlgorithmCrypt"); - return () -> { - try - { - // Plain JCE, so that this also works before WSS4J/XMLSec was initialized - final KeyGenerator aKeyGen; - if (eAlgorithmCrypt == ECryptoAlgorithmCrypt.CRYPT_3DES) - aKeyGen = KeyGenerator.getInstance ("DESede"); - else - { - aKeyGen = KeyGenerator.getInstance ("AES"); - aKeyGen.init (eAlgorithmCrypt.getKeySizeBits ()); - } - return aKeyGen.generateKey (); - } - catch (final NoSuchAlgorithmException ex) - { - throw new IllegalStateException ("Failed to create session key (" + eAlgorithmCrypt.getID () + ")", ex); - } - }; + return () -> eAlgorithmCrypt.createKeyGenerator ().generateKey (); } } 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 77d1c9e47..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 @@ -235,4 +235,25 @@ public void testCloneKeepsDerivedSessionKeyProvider () 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); + } + } }