Skip to content
Merged
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 @@ -81,7 +81,8 @@ public class AS4CryptParams implements ICloneable <AS4CryptParams>
// 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;
Expand Down Expand Up @@ -411,28 +412,46 @@ public final AS4CryptParams setAlias (@Nullable final String sAlias)
}

/**
* @return The session key provider to be used. Never <code>null</code>.
* @return The session key provider to be used. Never <code>null</code>. 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 <code>true</code> if a session key provider was set explicitly via
* {@link #setSessionKeyProvider(ICryptoSessionKeyProvider)}, <code>false</code> if it is
* derived from the encryption algorithm.
* @since 4.8.0
*/
public final boolean hasExplicitSessionKeyProvider ()
{
return m_aSessionKeyProvider != null;
}

/**
* Set the session key provider to be used for encryption. The provided provider must never return
* a <code>null</code> key.
*
* @param aSessionKeyProvider
* The session key provider to be used. May not be <code>null</code>.
* The session key provider to be used. May be <code>null</code> (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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
Expand All @@ -53,15 +83,43 @@
private final String m_sID;
private final String m_sOID;
private final String m_sAlgorithmURI;
private final int m_nKeySizeBits;
private final Supplier <KeyGenerator> 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 <KeyGenerator> 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
Expand All @@ -80,7 +138,7 @@

@NonNull
@Deprecated (since = "4.6.1")
public ASN1ObjectIdentifier getOID ()

Check warning on line 141 in phase4-lib/src/main/java/com/helger/phase4/crypto/ECryptoAlgorithmCrypt.java

View workflow job for this annotation

GitHub Actions / Java 21 build

getOID() in com.helger.phase4.crypto.ICryptoAlgorithmCrypt has been deprecated and marked for removal

Check warning on line 141 in phase4-lib/src/main/java/com/helger/phase4/crypto/ECryptoAlgorithmCrypt.java

View workflow job for this annotation

GitHub Actions / Java 17 build

getOID() in com.helger.phase4.crypto.ICryptoAlgorithmCrypt has been deprecated and marked for removal

Check warning on line 141 in phase4-lib/src/main/java/com/helger/phase4/crypto/ECryptoAlgorithmCrypt.java

View workflow job for this annotation

GitHub Actions / Java 25 build

getOID() in com.helger.phase4.crypto.ICryptoAlgorithmCrypt has been deprecated and marked for removal
{
ASN1ObjectIdentifier ret = m_aOID;
if (ret == null)
Expand All @@ -103,6 +161,30 @@
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 <code>null</code>.
* @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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 <code>null</code>.
* @return A new session key provider. Never <code>null</code>.
* @since 4.8.0
*/
@NonNull
static ICryptoSessionKeyProvider createRandomFor (@NonNull final ECryptoAlgorithmCrypt eAlgorithmCrypt)
{
ValueEnforcer.notNull (eAlgorithmCrypt, "AlgorithmCrypt");
return () -> eAlgorithmCrypt.createKeyGenerator ().generateKey ();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}
}
Loading
Loading