map)
+ {
+ return KeyProvider.of(PSK, map)::retrieve;
+ }
+}
diff --git a/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/Protocol.java b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/Protocol.java
new file mode 100644
index 0000000..5e3ed0e
--- /dev/null
+++ b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/Protocol.java
@@ -0,0 +1,18 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+public interface Protocol
+{
+ Mode getMode();
+
+ KemId getKemId();
+
+ KdfId getKdfId();
+
+ AeadId getAeadId();
+
+ ChunkLength getChunkLength();
+
+ byte[] getReceiverKeyId();
+
+ byte[] getKdfInfo();
+}
diff --git a/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolFactory.java b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolFactory.java
new file mode 100644
index 0000000..91c80ea
--- /dev/null
+++ b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolFactory.java
@@ -0,0 +1,140 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public class ProtocolFactory
+{
+ public static final byte[] MAGIC = new byte[] { 'H', 'P', 'K', 'E', 'F' };
+
+ public static final int ROOT_HEADER_LENGTH = MAGIC.length + 1;
+
+ public static interface ProtocolSerializer
+ {
+ int getVersion();
+
+ P read(InputStream source) throws IOException;
+
+ Class
getType();
+
+ byte[] write(P protocol);
+ }
+
+ public static final ProtocolSerializer V1_SERIALIZER = new ProtocolSerializer<>()
+ {
+ @Override
+ public int getVersion()
+ {
+ return ProtocolV1.VERSION;
+ }
+
+ @Override
+ public ProtocolV1 read(InputStream source) throws IOException
+ {
+ return ProtocolV1.from(source);
+ }
+
+ @Override
+ public Class getType()
+ {
+ return ProtocolV1.class;
+ }
+
+ @Override
+ public byte[] write(ProtocolV1 protocol)
+ {
+ return protocol.getCanonicalHeader();
+ }
+ };
+
+ private final PreSharedKeyProvider preSharedKeyProvider;
+ private final ReceiverPrivateKeyProvider receiverPrivateKeyProvider;
+
+ private final List> protocolSerializers = new ArrayList<>();
+
+ /**
+ * @param preSharedKeyProvider
+ * not null
+ * @param receiverPrivateKeyProvider
+ * not null
+ */
+ public ProtocolFactory(PreSharedKeyProvider preSharedKeyProvider,
+ ReceiverPrivateKeyProvider receiverPrivateKeyProvider)
+ {
+ this(preSharedKeyProvider, receiverPrivateKeyProvider, List.of(V1_SERIALIZER));
+ }
+
+ protected ProtocolFactory(PreSharedKeyProvider preSharedKeyProvider,
+ ReceiverPrivateKeyProvider receiverPrivateKeyProvider,
+ Collection extends ProtocolSerializer extends Protocol>> protocolSerializers)
+ {
+ this.preSharedKeyProvider = Objects.requireNonNull(preSharedKeyProvider, "preSharedKeyProvider");
+ this.receiverPrivateKeyProvider = Objects.requireNonNull(receiverPrivateKeyProvider,
+ "receiverPrivateKeyProvider");
+
+ if (protocolSerializers != null)
+ {
+ this.protocolSerializers.addAll(protocolSerializers);
+
+ if (protocolSerializers.size() != protocolSerializers.stream().mapToInt(ProtocolSerializer::getVersion)
+ .distinct().count())
+ throw new IllegalArgumentException("Multiple protocol serializers for same version");
+ }
+ }
+
+ public Protocol read(InputStream source) throws IOException
+ {
+ Objects.requireNonNull(source, "source");
+
+ byte[] baseHeaderValue = source.readNBytes(ROOT_HEADER_LENGTH);
+ ByteEncoding.expectRead(ROOT_HEADER_LENGTH, baseHeaderValue.length);
+
+ if (!Arrays.equals(MAGIC, 0, MAGIC.length, baseHeaderValue, 0, MAGIC.length))
+ throw new IOException("Protocol not supported");
+
+ int version = baseHeaderValue[baseHeaderValue.length - 1] & 0xFF;
+
+ Optional> deserializer = protocolSerializers.stream()
+ .filter(s -> s.getVersion() == version).findFirst();
+ if (deserializer.isPresent())
+ return deserializer.get().read(source);
+ else
+ throw new IOException("Protocol not supported");
+ }
+
+ public InputStream write(Protocol protocol)
+ {
+ Objects.requireNonNull(protocol, "protocol");
+
+ Optional> serializer = protocolSerializers.stream()
+ .filter(s -> s.getType().isInstance(protocol)).findFirst();
+
+ if (serializer.isPresent())
+ {
+ @SuppressWarnings("unchecked")
+ ProtocolSerializer protocolSerializer = (ProtocolSerializer) serializer.get();
+
+ return new ByteArrayInputStream(ByteEncoding.concat(MAGIC,
+ ByteEncoding.i2osp1(protocolSerializer.getVersion()), protocolSerializer.write(protocol)));
+ }
+ else
+ throw new IllegalArgumentException("Protocol not supported");
+ }
+
+ public PreSharedKeyProvider getPreSharedKeyProvider()
+ {
+ return preSharedKeyProvider;
+ }
+
+ public ReceiverPrivateKeyProvider getReceiverPrivateKeyProvider()
+ {
+ return receiverPrivateKeyProvider;
+ }
+}
diff --git a/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolV1.java b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolV1.java
new file mode 100644
index 0000000..0e856a8
--- /dev/null
+++ b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolV1.java
@@ -0,0 +1,205 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * The wire-format header is defined with fixed length and fixed order:
+ * ["HPKEF", 5 bytes] (magic marker)
+ * [0x01, 1 byte]
+ * [mode (base, psk), 1 byte] - see {@link Mode}
+ * [kem-id, 2 bytes] - see {@link KemId}
+ * [kdf-id, 2 byte] - see {@link KdfId}
+ * [aead-id, 2 byte] - see {@link AeadId}
+ * [chunkLengthExponent, 1 byte] - see {@link ChunkLength}
+ * [receiver-key-id, 32 bytes]
+ * [pre-shared-key-id, 32 bytes] - only if mode = psk
+ *
+ * Uses ["HPKEF"][0x01][chunkLengthExponent] as KDF info value, see {@link #KDF_INFO}.
+ * Supported chunk lengths are defined in {@link ChunkLength}.
+ */
+public class ProtocolV1 implements Protocol
+{
+ public static final byte VERSION = (byte) 0x01;
+
+ // last byte: chunk length exponent
+ private static final byte[] KDF_INFO = new byte[] { 'H', 'P', 'K', 'E', 'F', VERSION, 0 };
+
+ public static final int RECEIVER_KEY_ID_LENGTH = 32;
+ public static final int PRE_SHARED_KEY_ID_LENGTH = 32;
+
+ public static final int HEADER_BASE_LENGTH = 8 + RECEIVER_KEY_ID_LENGTH;
+ public static final int HEADER_PSK_LENGTH = HEADER_BASE_LENGTH + PRE_SHARED_KEY_ID_LENGTH;
+
+ private final Mode mode;
+ private final KemId kemId;
+ private final KdfId kdfId;
+ private final AeadId aeadId;
+
+ private final ChunkLength chunkLength;
+ private final byte[] receiverKeyId;
+
+ private final AtomicReference canonical = new AtomicReference<>();
+
+ /**
+ * @param mode
+ * not null, if mode {@link Mode#PSK_VALUE}: mode.pskId.length =
+ * {@value #PRE_SHARED_KEY_ID_LENGTH}
+ * @param kemId
+ * not null
+ * @param kdfId
+ * not null
+ * @param aeadId
+ * not null
+ * @param chunkLength
+ * not null
+ * @param receiverKeyId
+ * not null, length {@value #RECEIVER_KEY_ID_LENGTH}
+ */
+ public ProtocolV1(Mode mode, KemId kemId, KdfId kdfId, AeadId aeadId, ChunkLength chunkLength, byte[] receiverKeyId)
+ {
+ Objects.requireNonNull(mode, "mode");
+ if (mode.isPsk() && mode.getPskId().length != ProtocolV1.PRE_SHARED_KEY_ID_LENGTH)
+ throw new IllegalArgumentException("mode.pskId.length not " + ProtocolV1.PRE_SHARED_KEY_ID_LENGTH);
+
+ Objects.requireNonNull(kemId, "kemId");
+ Objects.requireNonNull(kdfId, "kdfId");
+ Objects.requireNonNull(aeadId, "aeadId");
+ Objects.requireNonNull(chunkLength, "chunkLength");
+ Objects.requireNonNull(receiverKeyId, "receiverKeyId");
+
+ if (receiverKeyId.length != RECEIVER_KEY_ID_LENGTH)
+ throw new IllegalArgumentException("receiverKeyId.length not " + RECEIVER_KEY_ID_LENGTH);
+
+ this.mode = mode;
+ this.kemId = kemId;
+ this.kdfId = kdfId;
+ this.aeadId = aeadId;
+ this.chunkLength = chunkLength;
+ this.receiverKeyId = receiverKeyId;
+ }
+
+ public static ProtocolV1 from(InputStream source) throws IOException
+ {
+ int modeValue = source.read();
+ ByteEncoding.throwIfTruncated(modeValue);
+
+ int remainingHeaderLength;
+ if (Mode.BASE_VALUE == (byte) modeValue)
+ remainingHeaderLength = HEADER_BASE_LENGTH - 1;
+ else if (Mode.PSK_VALUE == (byte) modeValue)
+ remainingHeaderLength = HEADER_PSK_LENGTH - 1;
+ else
+ throw new IOException("Mode not supported");
+
+ byte[] remainingHeader = source.readNBytes(remainingHeaderLength);
+ ByteEncoding.expectRead(remainingHeaderLength, remainingHeader.length);
+
+ ByteBuffer buffer = ByteBuffer.wrap(remainingHeader);
+
+ byte[] kemIdValue = new byte[2];
+ byte[] kdfIdValue = new byte[2];
+ byte[] aeadIdValue = new byte[2];
+ byte[] chunkLengthValue = new byte[1];
+ byte[] receiverKeyId = new byte[ProtocolV1.RECEIVER_KEY_ID_LENGTH];
+
+ buffer.get(kemIdValue).get(kdfIdValue).get(aeadIdValue).get(chunkLengthValue).get(receiverKeyId);
+
+ byte[] pskId;
+ if (Mode.PSK_VALUE == modeValue)
+ {
+ pskId = new byte[ProtocolV1.PRE_SHARED_KEY_ID_LENGTH];
+ buffer.get(pskId);
+ }
+ else
+ pskId = null;
+
+ try
+ {
+ Mode mode = Mode.from((byte) modeValue, pskId);
+ KemId kemId = KemId.from(kemIdValue);
+ KdfId kdfId = KdfId.from(kdfIdValue);
+ AeadId aeadId = AeadId.from(aeadIdValue);
+ ChunkLength chunkLength = ChunkLength.from(chunkLengthValue);
+
+ return new ProtocolV1(mode, kemId, kdfId, aeadId, chunkLength, receiverKeyId);
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+
+ public byte[] getCanonicalHeader()
+ {
+ byte[] c = canonical.get();
+ if (c == null)
+ canonical.compareAndSet(null, toCanonical());
+
+ return canonical.get();
+ }
+
+ private byte[] toCanonical()
+ {
+ ByteBuffer buffer = ByteBuffer.allocate(mode.isPsk() ? HEADER_PSK_LENGTH : HEADER_BASE_LENGTH);
+ buffer.put(mode.getValueAsI2osp1Byte());
+ buffer.put(kemId.getIdAsI2osp2Bytes());
+ buffer.put(kdfId.getIdAsI2osp2Bytes());
+ buffer.put(aeadId.getIdAsI2osp2Bytes());
+ buffer.put(chunkLength.getExponentAsI2osp1Byte());
+ buffer.put(receiverKeyId);
+
+ if (mode.isPsk())
+ buffer.put(mode.getPskId());
+
+ return buffer.array();
+ }
+
+ @Override
+ public Mode getMode()
+ {
+ return mode;
+ }
+
+ @Override
+ public KemId getKemId()
+ {
+ return kemId;
+ }
+
+ @Override
+ public KdfId getKdfId()
+ {
+ return kdfId;
+ }
+
+ @Override
+ public AeadId getAeadId()
+ {
+ return aeadId;
+ }
+
+ @Override
+ public ChunkLength getChunkLength()
+ {
+ return chunkLength;
+ }
+
+ @Override
+ public byte[] getReceiverKeyId()
+ {
+ return receiverKeyId.clone();
+ }
+
+ @Override
+ public byte[] getKdfInfo()
+ {
+ byte[] kdfInfo = KDF_INFO.clone();
+ kdfInfo[KDF_INFO.length - 1] = chunkLength.getExponentAsI2osp1Byte()[0];
+
+ return kdfInfo;
+ }
+}
diff --git a/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/ReceiverPrivateKeyProvider.java b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/ReceiverPrivateKeyProvider.java
new file mode 100644
index 0000000..be8c171
--- /dev/null
+++ b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/ReceiverPrivateKeyProvider.java
@@ -0,0 +1,27 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import java.security.PrivateKey;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * {@link Function} to retrieve the receiver {@link PrivateKey} for a given receiverKeyId
+ */
+@FunctionalInterface
+public interface ReceiverPrivateKeyProvider extends KeyProvider
+{
+ static ReceiverPrivateKeyProvider of()
+ {
+ return KeyProvider. of(RECEIVER_KEY_ID)::retrieve;
+ }
+
+ static ReceiverPrivateKeyProvider of(byte[] receiverKeyId, PrivateKey receiverKey)
+ {
+ return KeyProvider.of(RECEIVER_KEY_ID, receiverKeyId, receiverKey)::retrieve;
+ }
+
+ static ReceiverPrivateKeyProvider of(Map map)
+ {
+ return KeyProvider.of(RECEIVER_KEY_ID, map)::retrieve;
+ }
+}
diff --git a/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/RsaKemWrapper.java b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/RsaKemWrapper.java
new file mode 100644
index 0000000..d324d04
--- /dev/null
+++ b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/RsaKemWrapper.java
@@ -0,0 +1,101 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.interfaces.RSAPublicKey;
+import java.util.function.Supplier;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.DecapsulateException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KEM.Encapsulated;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.bouncycastle.crypto.DataLengthException;
+import org.bouncycastle.crypto.DerivationFunction;
+import org.bouncycastle.crypto.SecretWithEncapsulation;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.bouncycastle.crypto.digests.SHA512Digest;
+import org.bouncycastle.crypto.generators.KDF2BytesGenerator;
+import org.bouncycastle.crypto.kems.RSAKEMGenerator;
+import org.bouncycastle.crypto.params.KDFParameters;
+import org.bouncycastle.crypto.params.RSAKeyParameters;
+
+/**
+ * Custom KEM implementation for KEM IDs {@link KemId#RSAKEM_1024_KDF2_SHA256}, {@link KemId#RSAKEM_2048_KDF2_SHA256},
+ * {@link KemId#RSAKEM_3072_KDF2_SHA512} and {@link KemId#RSAKEM_4096_KDF2_SHA512}. Not defined in RFC 9180 and thus not
+ * compatible with other RFC 9180 implementations.
+ *
+ * Uses RSA Key Encapsulation Mechanism (RSA-KEM) from ISO 18033-2 via {@link RSAKEMGenerator} for encapsulation and a
+ * raw RSA (r = c^d mod n) operation for decapsulation. The shared secret is derived via
+ * {@link KDF2BytesGenerator} and {@link SHA256Digest} for 1024 and 2048 bit RSA keys, or {@link SHA512Digest} for 3072
+ * and 4096 bit RSA keys.
+ */
+public class RsaKemWrapper extends AbstractKemWrapper implements KemWrapper
+{
+ private static final String SHARED_SECRET_KEY_ALGORITHM = "Generic";
+
+ private final Supplier derivationFunctionFactory;
+
+ public RsaKemWrapper(KemId kemId)
+ {
+ super(kemId);
+
+ derivationFunctionFactory = switch (kemId)
+ {
+ case RSAKEM_1024_KDF2_SHA256, RSAKEM_2048_KDF2_SHA256 -> () -> new KDF2BytesGenerator(new SHA256Digest());
+ case RSAKEM_3072_KDF2_SHA512, RSAKEM_4096_KDF2_SHA512 -> () -> new KDF2BytesGenerator(new SHA512Digest());
+
+ default -> throw new IllegalArgumentException("KemId " + kemId.name() + " not supported");
+ };
+ }
+
+ @Override
+ protected Encapsulated doGetEncapsulated(PublicKey publicKey, SecureRandom secureRandom, int sharedSecretLength)
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
+
+ RSAKeyParameters rsaKeyParameters = new RSAKeyParameters(false, rsaPublicKey.getModulus(),
+ rsaPublicKey.getPublicExponent());
+
+ RSAKEMGenerator encapsulator = new RSAKEMGenerator(sharedSecretLength, derivationFunctionFactory.get(),
+ secureRandom);
+ SecretWithEncapsulation encapsulated = encapsulator.generateEncapsulated(rsaKeyParameters);
+
+ return new Encapsulated(new SecretKeySpec(encapsulated.getSecret(), SHARED_SECRET_KEY_ALGORITHM),
+ encapsulated.getEncapsulation(), null);
+ }
+
+ @Override
+ protected SecretKey doGetSharedSecret(PrivateKey privateKey, byte[] encapsulation, int sharedSecretLength)
+ throws NoSuchAlgorithmException, InvalidKeyException, DecapsulateException
+ {
+ try
+ {
+ // no padding to get equivalent operation for: r = c^d mod n
+ Cipher cipher = Cipher.getInstance("RSA/ECB/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, privateKey);
+ byte[] r = cipher.doFinal(encapsulation);
+
+ DerivationFunction kdf = derivationFunctionFactory.get();
+ kdf.init(new KDFParameters(r, null));
+
+ byte[] secret = new byte[sharedSecretLength];
+ kdf.generateBytes(secret, 0, secret.length);
+
+ return new SecretKeySpec(secret, SHARED_SECRET_KEY_ALGORITHM);
+ }
+ catch (InvalidKeyException | DataLengthException | NoSuchAlgorithmException | NoSuchPaddingException
+ | IllegalBlockSizeException | BadPaddingException | IllegalArgumentException e)
+ {
+ throw new DecapsulateException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/RuntimeIOException.java b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/RuntimeIOException.java
new file mode 100644
index 0000000..051d50b
--- /dev/null
+++ b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/RuntimeIOException.java
@@ -0,0 +1,22 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import java.io.IOException;
+import java.util.Objects;
+
+public final class RuntimeIOException extends RuntimeException
+{
+ private static final long serialVersionUID = 1L;
+
+ public RuntimeIOException(IOException cause)
+ {
+ super(Objects.requireNonNull(cause, "cause"));
+ }
+
+ /**
+ * @return the wrapped {@link IOException}
+ */
+ public IOException asIOException()
+ {
+ return (IOException) super.getCause();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/SequenceInputStreamForRuntimeIOException.java b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/SequenceInputStreamForRuntimeIOException.java
new file mode 100644
index 0000000..26bf2fd
--- /dev/null
+++ b/src/main/java/de/hsheilbronn/mi/utils/crypto/hpke/SequenceInputStreamForRuntimeIOException.java
@@ -0,0 +1,63 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.SequenceInputStream;
+import java.util.Enumeration;
+
+public final class SequenceInputStreamForRuntimeIOException extends SequenceInputStream
+{
+ public static final SequenceInputStream of(ChunkedInputStreamEnumeration enumeration) throws IOException
+ {
+ return withRuntimeIOException(() -> new SequenceInputStreamForRuntimeIOException(enumeration));
+ }
+
+ /**
+ * @param e
+ * not null
+ * @throws RuntimeIOException
+ * if errors occur during the peek operation of the {@link SequenceInputStream} constructor an thus
+ * reading of the first element
+ */
+ private SequenceInputStreamForRuntimeIOException(Enumeration extends InputStream> e)
+ {
+ super(e);
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ return withRuntimeIOException(() -> super.read());
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException
+ {
+ return withRuntimeIOException(() -> super.read(b, off, len));
+ }
+
+ @Override
+ public long transferTo(OutputStream out) throws IOException
+ {
+ return withRuntimeIOException(() -> super.transferTo(out));
+ }
+
+ @FunctionalInterface
+ private static interface SupplierWithIOException
+ {
+ T get() throws IOException;
+ }
+
+ private static T withRuntimeIOException(SupplierWithIOException withRuntimeIOException) throws IOException
+ {
+ try
+ {
+ return withRuntimeIOException.get();
+ }
+ catch (RuntimeIOException e)
+ {
+ throw e.asIOException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/hsheilbronn/mi/utils/crypto/keypair/KeyPairGeneratorFactory.java b/src/main/java/de/hsheilbronn/mi/utils/crypto/keypair/KeyPairGeneratorFactory.java
index 099fd7b..d341e05 100644
--- a/src/main/java/de/hsheilbronn/mi/utils/crypto/keypair/KeyPairGeneratorFactory.java
+++ b/src/main/java/de/hsheilbronn/mi/utils/crypto/keypair/KeyPairGeneratorFactory.java
@@ -140,7 +140,7 @@ public static KeyPairGeneratorFactory rsa4096()
/**
* @param keySize
- * >= 1204, % 1024 == 0
+ * >= 1024, % 1024 == 0
* @return RSA {@link KeyPairGeneratorFactory} for {@link CertificateAuthority}
*/
public static KeyPairGeneratorFactory rsa(int keySize)
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 2562499..0c022cc 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -7,6 +7,7 @@
exports de.hsheilbronn.mi.utils.crypto.ca;
exports de.hsheilbronn.mi.utils.crypto.cert;
exports de.hsheilbronn.mi.utils.crypto.context;
+ exports de.hsheilbronn.mi.utils.crypto.hpke;
exports de.hsheilbronn.mi.utils.crypto.io;
exports de.hsheilbronn.mi.utils.crypto.kem;
exports de.hsheilbronn.mi.utils.crypto.keypair;
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/AbstractKemWrapperTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/AbstractKemWrapperTest.java
new file mode 100644
index 0000000..a00b256
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/AbstractKemWrapperTest.java
@@ -0,0 +1,116 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+
+import javax.crypto.DecapsulateException;
+import javax.crypto.KEM.Encapsulated;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.junit.jupiter.api.Test;
+
+import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory;
+
+public class AbstractKemWrapperTest
+{
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ private static AbstractKemWrapper createTestWrapper(KemId kemId, int testEncapsulationLength,
+ int testSharedSecretLength)
+ {
+ return new AbstractKemWrapper(kemId)
+ {
+ @Override
+ protected Encapsulated doGetEncapsulated(PublicKey publicKey, SecureRandom secureRandom,
+ int sharedSecretLength) throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ return new Encapsulated(createKey(testSharedSecretLength), new byte[testEncapsulationLength], null);
+ }
+
+ @Override
+ protected SecretKey doGetSharedSecret(PrivateKey privateKey, byte[] encapsulation, int sharedSecretLength)
+ throws NoSuchAlgorithmException, InvalidKeyException, DecapsulateException
+ {
+ return createKey(testSharedSecretLength);
+ }
+
+ private SecretKeySpec createKey(int sharedSecretLength)
+ {
+ return new SecretKeySpec(new byte[sharedSecretLength], "Generic");
+ }
+ };
+ }
+
+ @Test
+ void testKeyNotSupported() throws Exception
+ {
+ KemId kemId = KemId.DHKEM_P256_HKDF_SHA256;
+ AbstractKemWrapper wrapper = createTestWrapper(kemId, kemId.getEncapsulationLength(),
+ kemId.getSharedSecretLength());
+ KeyPair notOkKeyPair = KeyPairGeneratorFactory.secp384r1().initialize().generateKeyPair();
+ KeyPair okKeyPair = kemId.getKeyPairGeneratorFactory().initialize().generateKeyPair();
+
+ assertDoesNotThrow(() -> wrapper.getEncapsulated(okKeyPair.getPublic(), SECURE_RANDOM));
+ KeyNotSupportedException e = assertThrowsExactly(KeyNotSupportedException.class,
+ () -> wrapper.getEncapsulated(notOkKeyPair.getPublic(), SECURE_RANDOM));
+ assertEquals("publicKey not supported", e.getMessage());
+
+ assertDoesNotThrow(
+ () -> wrapper.getSharedSecret(okKeyPair.getPrivate(), new byte[kemId.getEncapsulationLength()]));
+ e = assertThrowsExactly(KeyNotSupportedException.class,
+ () -> wrapper.getSharedSecret(notOkKeyPair.getPrivate(), new byte[kemId.getEncapsulationLength()]));
+ assertEquals("privateKey not supported", e.getMessage());
+ }
+
+ @Test
+ void testBadEncapsulationLength() throws Exception
+ {
+ KemId kemId = KemId.DHKEM_P256_HKDF_SHA256;
+ KeyPair okKeyPair = kemId.getKeyPairGeneratorFactory().initialize().generateKeyPair();
+
+ assertDoesNotThrow(() -> createTestWrapper(kemId, kemId.getEncapsulationLength(), kemId.getSharedSecretLength())
+ .getEncapsulated(okKeyPair.getPublic(), SECURE_RANDOM));
+ IllegalStateException isE = assertThrowsExactly(IllegalStateException.class,
+ () -> createTestWrapper(kemId, 0, kemId.getSharedSecretLength()).getEncapsulated(okKeyPair.getPublic(),
+ SECURE_RANDOM));
+ assertEquals("encapsulation.length not " + kemId.getEncapsulationLength(), isE.getMessage());
+
+ assertDoesNotThrow(() -> createTestWrapper(kemId, kemId.getEncapsulationLength(), kemId.getSharedSecretLength())
+ .getSharedSecret(okKeyPair.getPrivate(), new byte[kemId.getEncapsulationLength()]));
+
+ IllegalStateException iaE = assertThrowsExactly(IllegalStateException.class,
+ () -> createTestWrapper(kemId, kemId.getEncapsulationLength(), kemId.getSharedSecretLength())
+ .getSharedSecret(okKeyPair.getPrivate(), new byte[0]));
+ assertEquals("encapsulation.length not " + kemId.getEncapsulationLength(), iaE.getMessage());
+ }
+
+ @Test
+ void testBadSharedSecretLength() throws Exception
+ {
+ KemId kemId = KemId.DHKEM_P256_HKDF_SHA256;
+ KeyPair okKeyPair = kemId.getKeyPairGeneratorFactory().initialize().generateKeyPair();
+ assertDoesNotThrow(() -> createTestWrapper(kemId, kemId.getEncapsulationLength(), kemId.getSharedSecretLength())
+ .getEncapsulated(okKeyPair.getPublic(), SECURE_RANDOM));
+ IllegalStateException isE = assertThrowsExactly(IllegalStateException.class,
+ () -> createTestWrapper(kemId, kemId.getEncapsulationLength(), kemId.getSharedSecretLength() - 1)
+ .getEncapsulated(okKeyPair.getPublic(), SECURE_RANDOM));
+ assertEquals("sharedSecret.length not " + kemId.getSharedSecretLength(), isE.getMessage());
+
+ assertDoesNotThrow(() -> createTestWrapper(kemId, kemId.getEncapsulationLength(), kemId.getSharedSecretLength())
+ .getSharedSecret(okKeyPair.getPrivate(), new byte[kemId.getEncapsulationLength()]));
+
+ IllegalStateException iaE = assertThrowsExactly(IllegalStateException.class,
+ () -> createTestWrapper(kemId, kemId.getEncapsulationLength(), kemId.getSharedSecretLength() - 1)
+ .getSharedSecret(okKeyPair.getPrivate(), new byte[kemId.getEncapsulationLength()]));
+ assertEquals("sharedSecret.length not " + kemId.getSharedSecretLength(), iaE.getMessage());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/AeadIdTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/AeadIdTest.java
new file mode 100644
index 0000000..92ad108
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/AeadIdTest.java
@@ -0,0 +1,133 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.security.AlgorithmParameters;
+import java.security.spec.InvalidParameterSpecException;
+import java.util.stream.Stream;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.IvParameterSpec;
+
+import org.bouncycastle.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class AeadIdTest
+{
+ private static Stream forTestFrom()
+ {
+ return Stream.of(Arguments.of(AeadId.AES_128_GCM, 0x0001), Arguments.of(AeadId.AES_256_GCM, 0x0002),
+ Arguments.of(AeadId.ChaCha20Poly1305, 0x0003));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestFrom")
+ void testFrom(AeadId expected, int id) throws Exception
+ {
+ assertEquals(expected, AeadId.from(new byte[] { (byte) (id >>> 8), (byte) id }));
+ }
+
+ private static Stream forTestFromInvalid()
+ {
+ return Stream.of(Arguments.of(null, NullPointerException.class, "value"),
+ Arguments.of(new byte[0], IllegalArgumentException.class, "value.length not 2"),
+ Arguments.of(new byte[1], IllegalArgumentException.class, "value.length not 2"),
+ Arguments.of(new byte[2], IllegalArgumentException.class, "AeadId not supported"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestFromInvalid")
+ void testFromInvalid(byte[] invalid, Class extends Exception> exceptionClass, String exceptionMessage)
+ throws Exception
+ {
+ Exception exception = assertThrowsExactly(exceptionClass, () -> AeadId.from(invalid));
+ assertEquals(exceptionMessage, exception.getMessage());
+ }
+
+ @FunctionalInterface
+ private static interface CipherAlgorithmParametersEvaluator
+ {
+ void evaluate(AlgorithmParameters params, int expectedAuthTagLengthBits, byte[] expectedIv)
+ throws InvalidParameterSpecException;
+ }
+
+ private static Stream forTestGetter()
+ {
+ CipherAlgorithmParametersEvaluator gcmEvaluator = (params, expectedAuthTagLengthBits, expectedIv) ->
+ {
+ GCMParameterSpec parameterSpec = params.getParameterSpec(GCMParameterSpec.class);
+ assertNotNull(parameterSpec);
+ assertEquals(expectedAuthTagLengthBits, parameterSpec.getTLen());
+ assertArrayEquals(expectedIv, parameterSpec.getIV());
+ };
+
+ CipherAlgorithmParametersEvaluator chaCha20Poly1305Evaluator = (params, _, expectedIv) ->
+ {
+ IvParameterSpec parameterSpec = params.getParameterSpec(IvParameterSpec.class);
+ assertArrayEquals(expectedIv, parameterSpec.getIV());
+ };
+
+ return Stream.of(
+ Arguments.of(0x0001, "AES", "AES/GCM/NoPadding", 16, 12, 128, "GCM", gcmEvaluator, AeadId.AES_128_GCM),
+ Arguments.of(0x0002, "AES", "AES/GCM/NoPadding", 32, 12, 128, "GCM", gcmEvaluator, AeadId.AES_256_GCM),
+ Arguments.of(0x0003, "ChaCha20", "ChaCha20-Poly1305", 32, 12, 128, "ChaCha20-Poly1305",
+ chaCha20Poly1305Evaluator, AeadId.ChaCha20Poly1305));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestGetter")
+ void testGetter(int expectedId, String expectedKeyAlgorithm, String expectedCipherAlgorithm, int expectedKeyLength,
+ int expectedIvLength, int expectedAuthTagLengthBits, String cipherParameterAlgorithmName,
+ CipherAlgorithmParametersEvaluator cipherAlgorithmParametersEvaluator, AeadId aeadId) throws Exception
+ {
+ assertEquals(expectedId, aeadId.getId());
+ assertArrayEquals(new byte[] { (byte) (expectedId >>> 8), (byte) expectedId }, aeadId.getIdAsI2osp2Bytes());
+ assertEquals(expectedKeyAlgorithm, aeadId.getKeyAlgorithm());
+ assertEquals(expectedKeyLength, aeadId.getKeyLength());
+ assertArrayEquals(new byte[] { (byte) (expectedKeyLength >>> 8), (byte) expectedKeyLength },
+ aeadId.getKeyLengthAsI2osp2Bytes());
+ assertEquals(expectedIvLength, aeadId.getIvLength());
+ assertArrayEquals(new byte[] { (byte) (expectedIvLength >>> 8), (byte) expectedIvLength },
+ aeadId.getIvLengthAsI2osp2Bytes());
+ assertEquals(expectedAuthTagLengthBits, aeadId.getAuthenticationTagLengthBits());
+
+ Cipher cipher = aeadId.toCipher();
+ assertNotNull(cipher);
+ assertEquals(expectedCipherAlgorithm, cipher.getAlgorithm());
+
+ KeyGenerator keyGen = KeyGenerator.getInstance(aeadId.getKeyAlgorithm());
+ keyGen.init(aeadId.getKeyLength() * 8);
+ SecretKey secretKey = keyGen.generateKey();
+ byte[] iv = new byte[aeadId.getIvLength()];
+ Arrays.fill(iv, (byte) 0xAB);
+
+ assertDoesNotThrow(() -> aeadId.initEncryptionCipher(cipher, secretKey, iv));
+ assertNotNull(cipher.getIV());
+ assertArrayEquals(iv, cipher.getIV());
+ assertEquals(cipherParameterAlgorithmName, cipher.getParameters().getAlgorithm());
+ IllegalArgumentException e = assertThrowsExactly(IllegalArgumentException.class,
+ () -> aeadId.initEncryptionCipher(cipher, secretKey, new byte[0]));
+ assertEquals("iv.length not " + aeadId.getIvLength(), e.getMessage());
+
+ cipherAlgorithmParametersEvaluator.evaluate(cipher.getParameters(), expectedAuthTagLengthBits, iv);
+
+ assertDoesNotThrow(() -> aeadId.initDecryptionCipher(cipher, secretKey, iv));
+ assertNotNull(cipher.getIV());
+ assertArrayEquals(iv, cipher.getIV());
+ assertEquals(cipherParameterAlgorithmName, cipher.getParameters().getAlgorithm());
+ e = assertThrowsExactly(IllegalArgumentException.class,
+ () -> aeadId.initDecryptionCipher(cipher, secretKey, new byte[0]));
+ assertEquals("iv.length not " + aeadId.getIvLength(), e.getMessage());
+
+ cipherAlgorithmParametersEvaluator.evaluate(cipher.getParameters(), expectedAuthTagLengthBits, iv);
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ByteEncodingTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ByteEncodingTest.java
new file mode 100644
index 0000000..0989679
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ByteEncodingTest.java
@@ -0,0 +1,144 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+
+public class ByteEncodingTest
+{
+ @Test
+ void testI2osp1() throws Exception
+ {
+ IllegalArgumentException e = assertThrowsExactly(IllegalArgumentException.class,
+ () -> ByteEncoding.i2osp1(Integer.MIN_VALUE));
+ assertEquals("value < 0 or value > 255", e.getMessage());
+ e = assertThrowsExactly(IllegalArgumentException.class, () -> ByteEncoding.i2osp1(-1));
+ assertEquals("value < 0 or value > 255", e.getMessage());
+
+ byte[] b00 = ByteEncoding.i2osp1(0x00);
+ assertArrayEquals(new byte[1], b00);
+ byte[] b01 = ByteEncoding.i2osp1(0x01);
+ assertArrayEquals(new byte[] { (byte) 0x01 }, b01);
+ byte[] bFF = ByteEncoding.i2osp1(0xFF);
+ assertArrayEquals(new byte[] { (byte) 0xFF }, bFF);
+
+ e = assertThrowsExactly(IllegalArgumentException.class, () -> ByteEncoding.i2osp1(0xFF + 1));
+ assertEquals("value < 0 or value > 255", e.getMessage());
+ e = assertThrowsExactly(IllegalArgumentException.class, () -> ByteEncoding.i2osp1(Integer.MAX_VALUE));
+ assertEquals("value < 0 or value > 255", e.getMessage());
+ }
+
+ @Test
+ void testI2osp2() throws Exception
+ {
+ IllegalArgumentException e = assertThrowsExactly(IllegalArgumentException.class,
+ () -> ByteEncoding.i2osp2(Integer.MIN_VALUE));
+ assertEquals("value < 0 or value > 65535", e.getMessage());
+ e = assertThrowsExactly(IllegalArgumentException.class, () -> ByteEncoding.i2osp2(-1));
+ assertEquals("value < 0 or value > 65535", e.getMessage());
+
+ byte[] b0000 = ByteEncoding.i2osp2(0x0000);
+ assertArrayEquals(new byte[2], b0000);
+ byte[] b0001 = ByteEncoding.i2osp2(0x0001);
+ assertArrayEquals(new byte[] { (byte) 0x00, (byte) 0x01 }, b0001);
+ byte[] bFFFF = ByteEncoding.i2osp2(0xFFFF);
+ assertArrayEquals(new byte[] { (byte) 0xFF, (byte) 0xFF }, bFFFF);
+
+ e = assertThrowsExactly(IllegalArgumentException.class, () -> ByteEncoding.i2osp2(0xFFFF + 1));
+ assertEquals("value < 0 or value > 65535", e.getMessage());
+ e = assertThrowsExactly(IllegalArgumentException.class, () -> ByteEncoding.i2osp2(Integer.MAX_VALUE));
+ assertEquals("value < 0 or value > 65535", e.getMessage());
+ }
+
+ @Test
+ void testOs2ip() throws Exception
+ {
+ assertThrowsExactly(NullPointerException.class, () -> ByteEncoding.os2ip(null));
+
+ IllegalArgumentException e = assertThrowsExactly(IllegalArgumentException.class,
+ () -> ByteEncoding.os2ip(new byte[0]));
+ assertEquals("input.length < 1 or input.length > 4", e.getMessage());
+
+ long i1_00 = ByteEncoding.os2ip(new byte[1]);
+ assertEquals(0x00, i1_00);
+ long i1_01 = ByteEncoding.os2ip(new byte[] { (byte) 0x01 });
+ assertEquals(0x01, i1_01);
+ long i1_FF = ByteEncoding.os2ip(new byte[] { (byte) 0xFF });
+ assertEquals(0xFF, i1_FF);
+
+ long i2_0000 = ByteEncoding.os2ip(new byte[2]);
+ assertEquals(0x0000, i2_0000);
+ long i2_0001 = ByteEncoding.os2ip(new byte[] { (byte) 0x00, (byte) 0x01 });
+ assertEquals(0x0001, i2_0001);
+ long i2_FFFF = ByteEncoding.os2ip(new byte[] { (byte) 0xFF, (byte) 0xFF });
+ assertEquals(0xFFFF, i2_FFFF);
+
+ long i3_000000 = ByteEncoding.os2ip(new byte[3]);
+ assertEquals(0x000000, i3_000000);
+ long i3_000001 = ByteEncoding.os2ip(new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x01 });
+ assertEquals(0x000001, i3_000001);
+ long i3_FFFFFF = ByteEncoding.os2ip(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF });
+ assertEquals(0xFFFFFF, i3_FFFFFF);
+
+ long i4_00000000 = ByteEncoding.os2ip(new byte[4]);
+ assertEquals(0x000000, i4_00000000);
+ long i4_00000001 = ByteEncoding.os2ip(new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01 });
+ assertEquals(0x000001, i4_00000001);
+ long i4_FFFFFFFF = ByteEncoding.os2ip(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF });
+ assertEquals(0xFFFFFFFFL, i4_FFFFFFFF);
+
+ e = assertThrowsExactly(IllegalArgumentException.class, () -> ByteEncoding.os2ip(new byte[5]));
+ assertEquals("input.length < 1 or input.length > 4", e.getMessage());
+ }
+
+ @Test
+ void testConcat() throws Exception
+ {
+ assertThrowsExactly(NullPointerException.class, () -> ByteEncoding.concat((byte[]) null));
+ assertThrowsExactly(NullPointerException.class, () -> ByteEncoding.concat(new byte[0], (byte[]) null));
+ assertThrowsExactly(NullPointerException.class,
+ () -> ByteEncoding.concat(new byte[0], (byte[]) null, new byte[0]));
+
+ assertArrayEquals(new byte[0], ByteEncoding.concat(new byte[0]));
+ assertArrayEquals(new byte[0], ByteEncoding.concat(new byte[0], new byte[0]));
+ assertArrayEquals(new byte[1], ByteEncoding.concat(new byte[0], new byte[1]));
+ assertArrayEquals(new byte[1], ByteEncoding.concat(new byte[1], new byte[0]));
+ assertArrayEquals(new byte[2], ByteEncoding.concat(new byte[1], new byte[1]));
+ assertArrayEquals(new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03 }, ByteEncoding
+ .concat(new byte[] { (byte) 0x01 }, new byte[] { (byte) 0x02 }, new byte[] { (byte) 0x03 }));
+ }
+
+ @Test
+ void testExpectRead() throws Exception
+ {
+ assertDoesNotThrow(() -> ByteEncoding.expectRead(0, 0));
+ IOException e = assertThrowsExactly(IOException.class, () -> ByteEncoding.expectRead(1, 0));
+ assertEquals(e.getMessage(), "Truncated stream");
+ e = assertThrowsExactly(IOException.class, () -> ByteEncoding.expectRead(1, -1));
+ assertEquals(e.getMessage(), "Truncated stream");
+ IllegalArgumentException i = assertThrowsExactly(IllegalArgumentException.class,
+ () -> ByteEncoding.expectRead(-1, 0));
+ assertEquals(i.getMessage(), "expected < 0");
+ }
+
+ @Test
+ void testThrowIfTruncated() throws Exception
+ {
+ IOException e = assertThrowsExactly(IOException.class, () -> ByteEncoding.throwIfTruncated(Integer.MIN_VALUE));
+ assertEquals(e.getMessage(), "Truncated stream");
+ e = assertThrowsExactly(IOException.class, () -> ByteEncoding.throwIfTruncated(-1));
+ assertEquals(e.getMessage(), "Truncated stream");
+
+ assertDoesNotThrow(() -> ByteEncoding.throwIfTruncated(0));
+ assertDoesNotThrow(() -> ByteEncoding.throwIfTruncated(1));
+ assertDoesNotThrow(() -> ByteEncoding.throwIfTruncated(0xFF));
+
+ e = assertThrowsExactly(IOException.class, () -> ByteEncoding.throwIfTruncated(Integer.MAX_VALUE));
+ assertEquals(e.getMessage(), "value > 255");
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ChunkLengthTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ChunkLengthTest.java
new file mode 100644
index 0000000..0a60da8
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ChunkLengthTest.java
@@ -0,0 +1,51 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.util.EnumSet;
+
+import org.junit.jupiter.api.Test;
+
+public class ChunkLengthTest
+{
+ @Test
+ void testEncodeDecode() throws Exception
+ {
+ EnumSet.allOf(ChunkLength.class).forEach(cl ->
+ {
+ byte[] encoded = cl.getExponentAsI2osp1Byte();
+ assertNotNull(encoded);
+ ChunkLength decoded = ChunkLength.from(encoded);
+ assertNotNull(decoded);
+ assertEquals(cl, decoded);
+ });
+ }
+
+ @Test
+ void testGetLenght() throws Exception
+ {
+ ChunkLength[] values = ChunkLength.values();
+ assertEquals(16, values.length);
+
+ for (int i = 0; i < values.length; i++)
+ {
+ int expectedLength = (int) (ChunkLength.BASE * Math.pow(2, i));
+ assertEquals(expectedLength, values[i].getLength());
+ }
+ }
+
+ @Test
+ void testFromInvalid() throws Exception
+ {
+ IllegalArgumentException e = assertThrowsExactly(IllegalArgumentException.class,
+ () -> ChunkLength.from(new byte[0]));
+ assertEquals("value.length not 1", e.getMessage());
+ e = assertThrowsExactly(IllegalArgumentException.class,
+ () -> ChunkLength.from(new byte[] { (byte) ChunkLength.values().length }));
+ assertEquals("Chunk length exponent not supported", e.getMessage());
+ e = assertThrowsExactly(IllegalArgumentException.class, () -> ChunkLength.from(new byte[] { (byte) 0xFF }));
+ assertEquals("Chunk length exponent not supported", e.getMessage());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ChunkedInputStreamEnumerationTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ChunkedInputStreamEnumerationTest.java
new file mode 100644
index 0000000..a9a0433
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ChunkedInputStreamEnumerationTest.java
@@ -0,0 +1,361 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.SequenceInputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.hsheilbronn.mi.utils.crypto.hpke.ChunkedInputStreamEnumeration.CryptOperation;
+
+public class ChunkedInputStreamEnumerationTest
+{
+ @Test
+ void testConstructor() throws Exception
+ {
+ CryptOperation op = (_, _, _, _) -> null;
+ byte[] baseNonce = new byte[1];
+ InputStream source = InputStream.nullInputStream();
+
+ assertDoesNotThrow(() -> new ChunkedInputStreamEnumeration(1, baseNonce, source, op));
+ assertThrowsExactly(IllegalArgumentException.class,
+ () -> new ChunkedInputStreamEnumeration(Integer.MIN_VALUE, baseNonce, source, op));
+ assertThrowsExactly(IllegalArgumentException.class,
+ () -> new ChunkedInputStreamEnumeration(-1, baseNonce, source, op));
+ assertThrowsExactly(IllegalArgumentException.class,
+ () -> new ChunkedInputStreamEnumeration(0, baseNonce, source, op));
+
+ assertThrowsExactly(NullPointerException.class, () -> new ChunkedInputStreamEnumeration(1, null, source, op));
+ assertThrowsExactly(IllegalArgumentException.class,
+ () -> new ChunkedInputStreamEnumeration(1, new byte[0], source, op));
+ assertThrowsExactly(NullPointerException.class,
+ () -> new ChunkedInputStreamEnumeration(1, baseNonce, null, op));
+ assertThrowsExactly(NullPointerException.class,
+ () -> new ChunkedInputStreamEnumeration(1, baseNonce, source, null));
+ }
+
+ @Test
+ void testChunking() throws Exception
+ {
+ CryptOperation op = (iv, sequence, finished, chunk) ->
+ {
+ assertNotNull(iv);
+ assertEquals(2, iv.length);
+ assertNotNull(sequence);
+ assertEquals(2, sequence.length);
+ assertNotNull(chunk);
+ assertEquals(1, chunk.length);
+
+ ByteBuffer b = ByteBuffer.allocate(2 + 2 + 1 + 1);
+
+ b.put(iv);
+ b.put(sequence);
+ b.put(finished ? (byte) 0x01 : (byte) 0x00);
+ b.put(chunk);
+
+ return new ByteArrayInputStream(b.array());
+ };
+ byte[] baseNonce = new byte[] { (byte) 0xFF, (byte) 0x00 };
+ InputStream source = new ByteArrayInputStream(new byte[] { (byte) 0xAA, (byte) 0xBB });
+
+ ChunkedInputStreamEnumeration e = new ChunkedInputStreamEnumeration(1, baseNonce, source, op);
+ ArrayList results = Collections.list(e);
+ assertEquals(2, results.size());
+ assertArrayEquals(HexFormat.of().parseHex("FF00000000AA"), results.get(0).readAllBytes());
+ assertArrayEquals(HexFormat.of().parseHex("FF01000101BB"), results.get(1).readAllBytes());
+ }
+
+ @Test
+ void testChunkingEmptyInput() throws Exception
+ {
+ CryptOperation op = (iv, sequence, finished, chunk) ->
+ {
+ assertNotNull(iv);
+ assertEquals(2, iv.length);
+ assertNotNull(sequence);
+ assertEquals(2, sequence.length);
+ assertNotNull(chunk);
+ assertEquals(0, chunk.length);
+
+ ByteBuffer b = ByteBuffer.allocate(2 + 2 + 1);
+
+ b.put(iv);
+ b.put(sequence);
+ b.put(finished ? (byte) 0x01 : (byte) 0x00);
+
+ return new ByteArrayInputStream(b.array());
+ };
+ byte[] baseNonce = new byte[] { (byte) 0xFF, (byte) 0x00 };
+ InputStream source = InputStream.nullInputStream();
+
+ ChunkedInputStreamEnumeration e = new ChunkedInputStreamEnumeration(1, baseNonce, source, op);
+ ArrayList results = Collections.list(e);
+ assertEquals(1, results.size());
+ assertArrayEquals(HexFormat.of().parseHex("FF00000001"), results.get(0).readAllBytes());
+ }
+
+ @Test
+ void testChunkingSourceShorterThenChunkLenght() throws Exception
+ {
+ CryptOperation op = (iv, sequence, finished, chunk) ->
+ {
+ assertNotNull(iv);
+ assertEquals(2, iv.length);
+ assertNotNull(sequence);
+ assertEquals(2, sequence.length);
+ assertNotNull(chunk);
+ assertTrue(chunk.length > 0);
+
+ ByteBuffer b = ByteBuffer.allocate(2 + 2 + 1 + chunk.length);
+
+ b.put(iv);
+ b.put(sequence);
+ b.put(finished ? (byte) 0x01 : (byte) 0x00);
+ b.put(chunk);
+
+ return new ByteArrayInputStream(b.array());
+ };
+ byte[] baseNonce = new byte[] { (byte) 0xFF, (byte) 0x00 };
+ InputStream source = new ByteArrayInputStream(new byte[] { (byte) 0xAA, (byte) 0xBB, (byte) 0xCC });
+
+ ChunkedInputStreamEnumeration e = new ChunkedInputStreamEnumeration(2, baseNonce, source, op);
+ ArrayList results = Collections.list(e);
+ assertEquals(2, results.size());
+ assertArrayEquals(HexFormat.of().parseHex("FF00000000AABB"), results.get(0).readAllBytes());
+ assertArrayEquals(HexFormat.of().parseHex("FF01000101CC"), results.get(1).readAllBytes());
+ }
+
+ @Test
+ void testChunkingSourceThrowsIOException() throws Exception
+ {
+ final int chunkLength = 2;
+ final IOException readException = new IOException("simmulated failed read");
+ final IOException closeException = new IOException("simmulated failed close");
+
+ CryptOperation op = (_, _, _, _) -> InputStream.nullInputStream();
+ byte[] baseNonce = new byte[] { (byte) 0xFF, (byte) 0x00 };
+ InputStream source = new ByteArrayInputStream(new byte[] { (byte) 0xAA, (byte) 0xBB, (byte) 0xCC });
+
+ InputStream sourceWrapper = new InputStream()
+ {
+ int counter = 0;
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException
+ {
+ if (counter >= chunkLength)
+ throw readException;
+
+ int i = source.read(b, off, len);
+ counter += i;
+ return i;
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ throw new IOException("Unexpected call of read() in test");
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ throw closeException;
+ }
+ };
+
+ ChunkedInputStreamEnumeration enumeration = new ChunkedInputStreamEnumeration(chunkLength, baseNonce,
+ sourceWrapper, op);
+ assertDoesNotThrow(() -> enumeration.hasMoreElements());
+ assertDoesNotThrow(() -> enumeration.nextElement());
+ assertDoesNotThrow(() -> enumeration.hasMoreElements());
+ RuntimeIOException e = assertThrowsExactly(RuntimeIOException.class, () -> enumeration.nextElement());
+ assertNotNull(e.asIOException());
+ assertEquals(readException, e.asIOException());
+ assertNotNull(e.asIOException().getSuppressed());
+ assertEquals(1, e.asIOException().getSuppressed().length);
+ assertEquals(closeException, e.asIOException().getSuppressed()[0]);
+ }
+
+ private static final HexFormat HEX = HexFormat.of();
+
+ private static record TestVector(AeadId aeadId, SecretKey key, byte[] baseNonce, Map data)
+ {
+ static TestVector of(AeadId aeadId, String keyHex, String baseNonceHex, Map data)
+ {
+ SecretKeySpec key = new SecretKeySpec(HEX.parseHex(keyHex), aeadId.getKeyAlgorithm());
+ return new TestVector(aeadId, key, HEX.parseHex(baseNonceHex), data);
+ }
+ }
+
+ private static record TestVectorData(byte[] aad, byte[] nonce, byte[] ct)
+ {
+ static TestVectorData of(String aadHex, String nonceHex, String ctHex)
+ {
+ return new TestVectorData(HEX.parseHex(aadHex), HEX.parseHex(nonceHex), HEX.parseHex(ctHex));
+ }
+ }
+
+ // Test Data from https://www.rfc-editor.org/rfc/rfc9180.html#name-test-vectors
+
+ private static final byte[] PT = HexFormat.of()
+ .parseHex("4265617574792069732074727574682c20747275746820626561757479");
+
+ private static final String aad0 = "436f756e742d30";
+ private static final String aad1 = "436f756e742d31";
+ private static final String aad2 = "436f756e742d32";
+ private static final String aad4 = "436f756e742d34";
+ private static final String aad255 = "436f756e742d323535";
+ private static final String aad256 = "436f756e742d323536";
+
+ // A.1.1.1. Encryptions
+ private static final Map A1_DATA = Map.of(0,
+ TestVectorData.of(aad0, "56d890e5accaaf011cff4b7d",
+ "f938558b5d72f1a23810b4be2ab4f84331acc02fc97babc53a52ae8218a355a96d8770ac83d07bea87e13c512a"),
+ 1,
+ TestVectorData.of(aad1, "56d890e5accaaf011cff4b7c",
+ "af2d7e9ac9ae7e270f46ba1f975be53c09f8d875bdc8535458c2494e8a6eab251c03d0c22a56b8ca42c2063b84"),
+ 2,
+ TestVectorData.of(aad2, "56d890e5accaaf011cff4b7f",
+ "498dfcabd92e8acedc281e85af1cb4e3e31c7dc394a1ca20e173cb72516491588d96a19ad4a683518973dcc180"),
+ 4,
+ TestVectorData.of(aad4, "56d890e5accaaf011cff4b79",
+ "583bd32bc67a5994bb8ceaca813d369bca7b2a42408cddef5e22f880b631215a09fc0012bc69fccaa251c0246d"),
+ 255,
+ TestVectorData.of(aad255, "56d890e5accaaf011cff4b82",
+ "7175db9717964058640a3a11fb9007941a5d1757fda1a6935c805c21af32505bf106deefec4a49ac38d71c9e0a"),
+ 256, TestVectorData.of(aad256, "56d890e5accaaf011cff4a7d",
+ "957f9800542b0b8891badb026d79cc54597cb2d225b54c00c5238c25d05c30e3fbeda97d2e0e1aba483a2df9f2"));
+
+ // A.2.1.1. Encryptions
+ private static final Map A2_DATA = Map.of(0,
+ TestVectorData.of(aad0, "5c4d98150661b848853b547f",
+ "1c5250d8034ec2b784ba2cfd69dbdb8af406cfe3ff938e131f0def8c8b60b4db21993c62ce81883d2dd1b51a28"),
+ 1,
+ TestVectorData.of(aad1, "5c4d98150661b848853b547e",
+ "6b53c051e4199c518de79594e1c4ab18b96f081549d45ce015be002090bb119e85285337cc95ba5f59992dc98c"),
+ 2,
+ TestVectorData.of(aad2, "5c4d98150661b848853b547d",
+ "71146bd6795ccc9c49ce25dda112a48f202ad220559502cef1f34271e0cb4b02b4f10ecac6f48c32f878fae86b"),
+ 4,
+ TestVectorData.of(aad4, "5c4d98150661b848853b547b",
+ "63357a2aa291f5a4e5f27db6baa2af8cf77427c7c1a909e0b37214dd47db122bb153495ff0b02e9e54a50dbe16"),
+ 255,
+ TestVectorData.of(aad255, "5c4d98150661b848853b5480",
+ "18ab939d63ddec9f6ac2b60d61d36a7375d2070c9b683861110757062c52b8880a5f6b3936da9cd6c23ef2a95c"),
+ 256, TestVectorData.of(aad256, "5c4d98150661b848853b557f",
+ "7a4a13e9ef23978e2c520fd4d2e757514ae160cd0cd05e556ef692370ca53076214c0c40d4c728d6ed9e727a5b"));
+
+ // A.6.1.1. Encryptions
+ private static final Map A6_DATA = Map.of(0,
+ TestVectorData.of(aad0, "55ff7a7d739c69f44b25447b",
+ "170f8beddfe949b75ef9c387e201baf4132fa7374593dfafa90768788b7b2b200aafcc6d80ea4c795a7c5b841a"),
+ 1,
+ TestVectorData.of(aad1, "55ff7a7d739c69f44b25447a",
+ "d9ee248e220ca24ac00bbbe7e221a832e4f7fa64c4fbab3945b6f3af0c5ecd5e16815b328be4954a05fd352256"),
+ 2,
+ TestVectorData.of(aad2, "55ff7a7d739c69f44b254479",
+ "142cf1e02d1f58d9285f2af7dcfa44f7c3f2d15c73d460c48c6e0e506a3144bae35284e7e221105b61d24e1c7a"),
+ 4,
+ TestVectorData.of(aad4, "55ff7a7d739c69f44b25447f",
+ "3bb3a5a07100e5a12805327bf3b152df728b1c1be75a9fd2cb2bf5eac0cca1fb80addb37eb2a32938c7268e3e5"),
+ 255,
+ TestVectorData.of(aad255, "55ff7a7d739c69f44b254484",
+ "4f268d0930f8d50b8fd9d0f26657ba25b5cb08b308c92e33382f369c768b558e113ac95a4c70dd60909ad1adc7"),
+ 256, TestVectorData.of(aad256, "55ff7a7d739c69f44b25457b",
+ "dbbfc44ae037864e75f136e8b4b4123351d480e6619ae0e0ae437f036f2f8f1ef677686323977a1ccbb4b4f16a"));
+
+ private static final TestVector A1 = TestVector.of(AeadId.AES_128_GCM, "4531685d41d65f03dc48f6b8302c05b0",
+ "56d890e5accaaf011cff4b7d", A1_DATA);
+ private static final TestVector A2 = TestVector.of(AeadId.ChaCha20Poly1305,
+ "ad2744de8e17f4ebba575b3f5f5a8fa1f69c2a07f6e7500bc60ca6e3e3ec1c91", "5c4d98150661b848853b547f", A2_DATA);
+ private static final TestVector A6 = TestVector.of(AeadId.AES_256_GCM,
+ "751e346ce8f0ddb2305c8a2a85c70d5cf559c53093656be636b9406d4d7d1b70", "55ff7a7d739c69f44b25447b", A6_DATA);
+
+ private static Stream forTestWithRfcTestVector() throws KeyNotFoundException
+ {
+ return Stream.of(A1, A2, A6).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestWithRfcTestVector")
+ void testWithRfcTestVector(TestVector vector) throws Exception
+ {
+ AeadId aeadId = vector.aeadId();
+ AtomicInteger counter = new AtomicInteger();
+
+ Cipher cipher = aeadId.toCipher();
+ CryptOperation op = (iv, _, _, chunk) ->
+ {
+ int c = counter.getAndIncrement();
+
+ Optional.ofNullable(vector.data().get(c)).map(TestVectorData::nonce)
+ .ifPresent(n -> assertArrayEquals(n, iv));
+
+ aeadId.initEncryptionCipher(cipher, vector.key(), iv);
+ Optional.ofNullable(vector.data().get(c)).map(TestVectorData::aad).ifPresent(a -> cipher.updateAAD(a));
+
+ return new ByteArrayInputStream(cipher.doFinal(chunk));
+ };
+
+ InputStream source = new SequenceInputStream(Collections
+ .enumeration(IntStream.rangeClosed(0, 256).mapToObj(_ -> PT).map(ByteArrayInputStream::new).toList()));
+
+ ChunkedInputStreamEnumeration e = new ChunkedInputStreamEnumeration(PT.length, vector.baseNonce(), source, op);
+ List results = Collections.list(e).stream().map(readAllBytes()).toList();
+ assertEquals(257, results.size());
+
+ vector.data().forEach((i, data) -> assertArrayEquals(data.ct(), results.get(i)));
+ }
+
+ private Function readAllBytes()
+ {
+ return in ->
+ {
+ try
+ {
+ return in.readAllBytes();
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ };
+ }
+
+ @Test
+ void testSequenceLimit() throws Exception
+ {
+ ChunkedInputStreamEnumeration en = new ChunkedInputStreamEnumeration(1, new byte[1],
+ new ByteArrayInputStream(new byte[0xFF + 1]), (_, _, _, _) -> null);
+
+ RuntimeIOException e = assertThrowsExactly(RuntimeIOException.class, () -> Collections.list(en));
+ assertEquals("Message limit reached", e.asIOException().getMessage());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/DhKemWrapperTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/DhKemWrapperTest.java
new file mode 100644
index 0000000..ac151db
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/DhKemWrapperTest.java
@@ -0,0 +1,201 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.stream.Stream;
+
+import javax.crypto.DecapsulateException;
+import javax.crypto.KEM.Encapsulated;
+import javax.crypto.SecretKey;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory;
+
+public class DhKemWrapperTest
+{
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ private static Stream forTestConstructor()
+ {
+ return EnumSet.complementOf(DhKemWrapper.DH_KEMS).stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestConstructor")
+ void testConstructor(KemId kemId) throws Exception
+ {
+ IllegalArgumentException e = assertThrowsExactly(IllegalArgumentException.class, () -> new DhKemWrapper(kemId));
+ assertEquals("KemId " + kemId.name() + " not supported", e.getMessage());
+ }
+
+ private static Stream kemVariants()
+ {
+ return Stream.of(
+ Arguments.of(KemId.DHKEM_P256_HKDF_SHA256,
+ KeyPairGeneratorFactory.secp256r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_P384_HKDF_SHA384,
+ KeyPairGeneratorFactory.secp384r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_P521_HKDF_SHA512,
+ KeyPairGeneratorFactory.secp521r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_X25519_HKDF_SHA256,
+ KeyPairGeneratorFactory.x25519().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_X448_HKDF_SHA512,
+ KeyPairGeneratorFactory.x448().initialize().generateKeyPair()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("kemVariants")
+ void testTruncatedEnc(KemId kemId, KeyPair keyPair) throws Exception
+ {
+ DhKemWrapper w = new DhKemWrapper(kemId);
+ Encapsulated encapsulated = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+
+ assertNotNull(encapsulated);
+
+ SecretKey sKey = encapsulated.key();
+ assertNotNull(sKey);
+ assertEquals("Generic", sKey.getAlgorithm());
+ assertNotNull(sKey.getEncoded());
+
+ byte[] encapsulation = encapsulated.encapsulation();
+ assertNotNull(encapsulation);
+ assertEquals(kemId.getEncapsulationLength(), encapsulation.length);
+
+ for (int i = 0; i <= encapsulation.length; i++)
+ {
+ try
+ {
+ w.getSharedSecret(keyPair.getPrivate(), Arrays.copyOfRange(encapsulation, 0, encapsulation.length - i));
+ assertEquals(0, i); // only not truncated stream ok
+ }
+ catch (IllegalStateException e)
+ {
+ assertEquals("encapsulation.length not " + kemId.getEncapsulationLength(), e.getMessage());
+ }
+ }
+ }
+
+ private static Stream forTestModifiedDheEnc()
+ {
+ return Stream.of(
+ Arguments.of(KemId.DHKEM_P256_HKDF_SHA256,
+ KeyPairGeneratorFactory.secp256r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_P384_HKDF_SHA384,
+ KeyPairGeneratorFactory.secp384r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_P521_HKDF_SHA512,
+ KeyPairGeneratorFactory.secp521r1().initialize().generateKeyPair()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestModifiedDheEnc")
+ void testModifiedDheEnc(KemId kemId, KeyPair keyPair) throws Exception
+ {
+ DhKemWrapper w = new DhKemWrapper(kemId);
+ Encapsulated encapsulated = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+
+ assertNotNull(encapsulated);
+
+ SecretKey sKey = encapsulated.key();
+ assertNotNull(sKey);
+ assertEquals("Generic", sKey.getAlgorithm());
+ assertNotNull(sKey.getEncoded());
+
+ byte[] encapsulation = encapsulated.encapsulation();
+ assertNotNull(encapsulation);
+ assertEquals(kemId.getEncapsulationLength(), encapsulation.length);
+
+ assertEquals(encapsulation[0], (byte) 0x04);
+
+ encapsulation[0] ^= 0x01;
+ DecapsulateException e = assertThrows(DecapsulateException.class,
+ () -> w.getSharedSecret(keyPair.getPrivate(), encapsulation));
+ assertEquals("Cannot decapsulate", e.getMessage());
+ assertNotNull(e.getCause());
+ assertEquals(IOException.class, e.getCause().getClass());
+
+ encapsulation[0] = (byte) 0x04;
+ encapsulation[1] ^= 0x01;
+ e = assertThrows(DecapsulateException.class, () -> w.getSharedSecret(keyPair.getPrivate(), encapsulation));
+ assertEquals("Cannot decapsulate", e.getMessage());
+ assertNotNull(e.getCause());
+ assertEquals(InvalidKeyException.class, e.getCause().getClass());
+ }
+
+ private static Stream forTestModifiedDhxEnc()
+ {
+ return Stream.of(
+ Arguments.of(KemId.DHKEM_X25519_HKDF_SHA256,
+ KeyPairGeneratorFactory.x25519().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_X448_HKDF_SHA512,
+ KeyPairGeneratorFactory.x448().initialize().generateKeyPair()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestModifiedDhxEnc")
+ void testModifiedDhxEnc(KemId kemId, KeyPair keyPair) throws Exception
+ {
+ DhKemWrapper w = new DhKemWrapper(kemId);
+ Encapsulated encapsulated = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+
+ assertNotNull(encapsulated);
+
+ SecretKey sKey = encapsulated.key();
+ assertNotNull(sKey);
+ assertEquals("Generic", sKey.getAlgorithm());
+ assertNotNull(sKey.getEncoded());
+
+ byte[] encapsulation = encapsulated.encapsulation();
+ assertNotNull(encapsulation);
+ assertEquals(kemId.getEncapsulationLength(), encapsulation.length);
+
+ encapsulation[0] ^= 0x01;
+
+ for (int i = 0; i < encapsulation.length; i++)
+ {
+ encapsulation[i] ^= 0x01;
+ SecretKey sharedSecret = w.getSharedSecret(keyPair.getPrivate(), encapsulation);
+
+ assertNotSame(sKey, sharedSecret);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("kemVariants")
+ void testTwoCallsResultInDifferentEncapsulationsAndKeys(KemId kemId, KeyPair keyPair) throws Exception
+ {
+ DhKemWrapper w = new DhKemWrapper(kemId);
+
+ Encapsulated e1 = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+ assertNotNull(e1);
+ assertNotNull(e1.encapsulation());
+ assertNotNull(e1.key());
+
+ Encapsulated e2 = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+ assertNotNull(e2);
+ assertNotNull(e2.encapsulation());
+ assertNotNull(e2.key());
+
+ assertFalse(Arrays.equals(e1.encapsulation(), e2.encapsulation()));
+ assertFalse(Arrays.equals(e1.key().getEncoded(), e2.key().getEncoded()));
+
+ SecretKey sharedSecret1 = w.getSharedSecret(keyPair.getPrivate(), e1.encapsulation());
+ assertArrayEquals(e1.key().getEncoded(), sharedSecret1.getEncoded());
+ SecretKey sharedSecret2 = w.getSharedSecret(keyPair.getPrivate(), e2.encapsulation());
+ assertArrayEquals(e2.key().getEncoded(), sharedSecret2.getEncoded());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/HpkeDemo.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/HpkeDemo.java
new file mode 100644
index 0000000..3c8081f
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/HpkeDemo.java
@@ -0,0 +1,30 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.KeyPair;
+
+import org.junit.jupiter.api.Test;
+
+public class HpkeDemo
+{
+ private static final int GIB_1 = 1024 * 1024 * 1024;
+
+ @Test
+ void v1WriteReadBaseModeDemo() throws Exception
+ {
+ byte[] receiverKeyId = new byte[ProtocolV1.RECEIVER_KEY_ID_LENGTH];
+ KemId kemId = KemId.DHKEM_X25519_HKDF_SHA256;
+ KeyPair keyPair = kemId.getKeyPairGeneratorFactory().initialize().generateKeyPair();
+ PreSharedKeyProvider preSharedKeyProvider = PreSharedKeyProvider.of();
+ ReceiverPrivateKeyProvider receiverPrivateKeyProvider = ReceiverPrivateKeyProvider.of(receiverKeyId,
+ keyPair.getPrivate());
+ ProtocolV1 protocol = new ProtocolV1(Mode.base(), kemId, KdfId.HKDF_SHA256, AeadId.AES_128_GCM,
+ ChunkLength.MiB_1, receiverKeyId);
+ ProtocolFactory protocolFactory = new ProtocolFactory(preSharedKeyProvider, receiverPrivateKeyProvider);
+ Hpke hpke = new Hpke(protocolFactory);
+
+ InputStream encrypted = hpke.encrypt(protocol, new ZeroInputStream(GIB_1), keyPair.getPublic());
+ hpke.decrypt(encrypted, OutputStream.nullOutputStream());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/HpkeTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/HpkeTest.java
new file mode 100644
index 0000000..a317289
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/HpkeTest.java
@@ -0,0 +1,461 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.DecapsulateException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.hsheilbronn.mi.utils.crypto.hpke.ProtocolFactory.ProtocolSerializer;
+
+public class HpkeTest
+{
+ private static final Logger logger = LoggerFactory.getLogger(HpkeTest.class);
+
+ private static final long TWO_KIB = 2L * 1024;
+
+ private static final byte[] PSK_ID = Sha256.digest("Test Pre Shared Key ID".getBytes(StandardCharsets.US_ASCII));
+ private static final SecretKey PSK = new SecretKeySpec(new byte[] { 'T', 'e', 's', 't', ' ', 'P', 'S', 'K' },
+ "Generic");
+ private static final PreSharedKeyProvider PRE_SHARED_KEY_PROVIDER = PreSharedKeyProvider.of(PSK_ID, PSK);
+
+ private static final byte[] RECEIVER_KEY_ID = Sha256
+ .digest("Test Receiver Key ID".getBytes(StandardCharsets.US_ASCII));
+
+ private static record ProtocolAndKeyPair(Protocol protocol, KeyPair keyPair)
+ {
+ @Override
+ public final String toString()
+ {
+ return Stream
+ .of("Mode " + protocol.getMode(), protocol.getKemId().name(), protocol.getKdfId().name(),
+ protocol.getAeadId().name(), protocol.getChunkLength().name())
+ .collect(Collectors.joining(", "));
+ }
+
+ Arguments toArguments(String plainText)
+ {
+ return Arguments.argumentSet(toString() + ", plainText: \"" + plainText + "\"", this, plainText);
+ }
+
+ Arguments toArguments()
+ {
+ return Arguments.argumentSet(toString(), this);
+ }
+ }
+
+ private static Stream forTestEncryptDecryptInputStream() throws KeyNotFoundException
+ {
+ List modes = List.of(Mode.base(), Mode.psk(PSK_ID));
+ KemId[] kemIds = EnumSet.of(KemId.RSAKEM_1024_KDF2_SHA256, KemId.RSAKEM_2048_KDF2_SHA256,
+ KemId.RSAKEM_3072_KDF2_SHA512, KemId.RSAKEM_4096_KDF2_SHA512).toArray(KemId[]::new);
+ KdfId[] kdfIds = KdfId.values();
+ AeadId[] aeadIds = AeadId.values();
+ ChunkLength[] chunkLengths = ChunkLength.values();
+
+ String plainText0 = "";
+ String plainText1 = "Foo Bar Baz";
+
+ return modes.stream().flatMap(mode ->
+ {
+ return Stream.of(kemIds).flatMap(kemId ->
+ {
+ KeyPair keyPair = kemId.getKeyPairGeneratorFactory().initialize().generateKeyPair();
+
+ return Stream.of(kdfIds).flatMap(kdfId ->
+ {
+ return Stream.of(aeadIds).flatMap(aeadId ->
+ {
+ return Stream.of(chunkLengths).flatMap(chunklength ->
+ {
+ return Stream.of(plainText0, plainText1).map(plainText ->
+ {
+ return new ProtocolAndKeyPair(
+ new ProtocolV1(mode, kemId, kdfId, aeadId, chunklength, RECEIVER_KEY_ID),
+ keyPair).toArguments(plainText);
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestEncryptDecryptInputStream")
+ void testEncryptDecryptInputStream(ProtocolAndKeyPair protocolAndKeyPair, String plainText) throws Exception
+ {
+ final Hpke hpke = new Hpke(new ProtocolFactory(PRE_SHARED_KEY_PROVIDER,
+ ReceiverPrivateKeyProvider.of(RECEIVER_KEY_ID, protocolAndKeyPair.keyPair().getPrivate())));
+
+ byte[] plainTextBytes = plainText.getBytes(StandardCharsets.UTF_8);
+ byte[] encrypted = hpke.encrypt(protocolAndKeyPair.protocol(), new ByteArrayInputStream(plainTextBytes),
+ protocolAndKeyPair.keyPair().getPublic()).readAllBytes();
+
+ logger.debug("{}, plaintText: \"{}\" - encrypted.length: {}", protocolAndKeyPair.toString(), plainText,
+ encrypted.length);
+
+ InputStream decryptedStream = hpke.decrypt(new ByteArrayInputStream(encrypted));
+ assertNotNull(decryptedStream);
+
+ byte[] decrypted = decryptedStream.readAllBytes();
+
+ assertArrayEquals(plainTextBytes, decrypted);
+ }
+
+ private static Stream forDecryptionTruncatedStreamTest() throws KeyNotFoundException
+ {
+ BiConsumer aesErrorHandler = (i, e) ->
+ {
+ if (i > 0 && i <= 1024 && e.getCause() instanceof AEADBadTagException badTag)
+ assertEquals("Tag mismatch", badTag.getMessage());
+ else if (i > 1024 && i <= 1024 + 15 && e.getCause() instanceof AEADBadTagException badTag)
+ assertEquals("Input data too short to contain an expected tag length of 16bytes", badTag.getMessage());
+ else if (i > 1024 + 15 && i <= 2048 + 16 && e.getCause() instanceof AEADBadTagException badTag)
+ assertEquals("Tag mismatch", badTag.getMessage());
+ else if (i > 2048 + 16 && i <= 2048 + 16 + 16 && e.getCause() instanceof AEADBadTagException badTag)
+ assertEquals("Input data too short to contain an expected tag length of 16bytes", badTag.getMessage());
+ else if (i > 2048 + 16 + 16)
+ assertEquals("Truncated stream", e.getMessage());
+ else
+ fail("Truncated by " + i, e);
+ };
+
+ BiConsumer chaCha20ErrorHandler = (i, e) ->
+ {
+ if (i > 0 && i <= 1024 && e.getCause() instanceof AEADBadTagException badTag)
+ assertEquals("Tag mismatch", badTag.getMessage());
+ else if (i > 1024 && i <= 1024 + 15 && e.getCause() instanceof AEADBadTagException badTag)
+ assertEquals("Input too short - need tag", badTag.getMessage());
+ else if (i > 1024 + 15 && i <= 2048 + 16 && e.getCause() instanceof AEADBadTagException badTag)
+ assertEquals("Tag mismatch", badTag.getMessage());
+ else if (i > 2048 + 16 && i <= 2048 + 16 + 16 && e.getCause() instanceof AEADBadTagException badTag)
+ assertEquals("Input too short - need tag", badTag.getMessage());
+ else if (i > 2048 + 16 + 16)
+ assertEquals("Truncated stream", e.getMessage());
+ else
+ fail("Truncated by " + i, e);
+ };
+
+ return Stream.of(AeadId.values()).map(aeadId ->
+ {
+ KemId kemId = KemId.RSAKEM_1024_KDF2_SHA256;
+ KeyPair keyPair = kemId.getKeyPairGeneratorFactory().initialize().generateKeyPair();
+ ProtocolV1 header = new ProtocolV1(handleException(() -> Mode.psk(PSK_ID)), kemId, KdfId.HKDF_SHA256,
+ aeadId, ChunkLength.KiB_1, RECEIVER_KEY_ID);
+ ProtocolAndKeyPair hkp = new ProtocolAndKeyPair(header, keyPair);
+
+ return Arguments.of(hkp, AeadId.ChaCha20Poly1305.equals(aeadId) ? chaCha20ErrorHandler : aesErrorHandler);
+ });
+ }
+
+ public interface SupplierWithException
+ {
+ T get() throws Exception;
+ }
+
+ private static Mode handleException(SupplierWithException modeSupplier)
+ {
+ try
+ {
+ return modeSupplier.get();
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("forDecryptionTruncatedStreamTest")
+ void decryptionTruncatedStreamTest(ProtocolAndKeyPair headerAndKeyPair,
+ BiConsumer errorHandler) throws Exception
+ {
+ final Hpke hpke = new Hpke(new ProtocolFactory(PRE_SHARED_KEY_PROVIDER,
+ ReceiverPrivateKeyProvider.of(RECEIVER_KEY_ID, headerAndKeyPair.keyPair().getPrivate())));
+
+ ByteArrayOutputStream encrypted = new ByteArrayOutputStream();
+ hpke.encrypt(headerAndKeyPair.protocol(), new ZeroInputStream(TWO_KIB), headerAndKeyPair.keyPair().getPublic(),
+ encrypted);
+
+ byte[] encryptedBytes = encrypted.toByteArray();
+ assertNotNull(encryptedBytes);
+
+ for (int i = 0; i <= encryptedBytes.length; i++)
+ {
+ try
+ {
+ hpke.decrypt(new ByteArrayInputStream(encryptedBytes, 0, encryptedBytes.length - i),
+ OutputStream.nullOutputStream());
+
+ assertEquals(0, i); // only not truncated stream ok
+ }
+ catch (IOException e)
+ {
+ errorHandler.accept(i, e);
+ }
+ catch (GeneralSecurityException e)
+ {
+ fail(e);
+ }
+ }
+ }
+
+ private static Stream forTestModifiedMessages() throws KeyNotFoundException
+ {
+ List modes = List.of(Mode.base(), Mode.psk(PSK_ID));
+ KemId[] kemIds = EnumSet.of(KemId.RSAKEM_1024_KDF2_SHA256, KemId.RSAKEM_2048_KDF2_SHA256,
+ KemId.RSAKEM_3072_KDF2_SHA512, KemId.RSAKEM_4096_KDF2_SHA512).toArray(KemId[]::new);
+ KdfId[] kdfIds = KdfId.values();
+ AeadId[] aeadIds = AeadId.values();
+ ChunkLength[] chunkLengths = ChunkLength.values();
+
+ return modes.stream().flatMap(mode ->
+ {
+ return Stream.of(kemIds).flatMap(kemId ->
+ {
+ KeyPair keyPair = kemId.getKeyPairGeneratorFactory().initialize().generateKeyPair();
+
+ return Stream.of(kdfIds).flatMap(kdfId ->
+ {
+ return Stream.of(aeadIds).flatMap(aeadId ->
+ {
+ return Stream.of(chunkLengths).map(chunkLength ->
+ {
+ return new ProtocolAndKeyPair(
+ new ProtocolV1(mode, kemId, kdfId, aeadId, chunkLength, RECEIVER_KEY_ID), keyPair)
+ .toArguments();
+ });
+ });
+ });
+ });
+ });
+
+ // KeyPair keyPair = kemIds[0].getKeyPairGeneratorFactory().initialize().generateKeyPair();
+ // return Stream.of(new ProtocolAndKeyPair(
+ // new ProtocolV1(modes.get(0), kemIds[0], kdfIds[0], aeadIds[0], chunkLengths[0], RECEIVER_KEY_ID),
+ // keyPair).toArguments());
+ }
+
+ private static final class TestProtocol extends ProtocolV1
+ {
+ public TestProtocol(Mode mode, KemId kemId, KdfId kdfId, AeadId aeadId, ChunkLength chunkLength,
+ byte[] receiverKeyId)
+ {
+ super(mode, kemId, kdfId, aeadId, chunkLength, receiverKeyId);
+ }
+
+ public static TestProtocol from(InputStream source) throws IOException
+ {
+ ProtocolV1 v1 = ProtocolV1.from(source);
+ return new TestProtocol(v1.getMode(), v1.getKemId(), v1.getKdfId(), v1.getAeadId(), v1.getChunkLength(),
+ v1.getReceiverKeyId());
+ }
+
+ @Override
+ public byte[] getKdfInfo()
+ {
+ byte[] kdfInfo = super.getKdfInfo();
+ kdfInfo[kdfInfo.length - 2] = 0x02;
+ return kdfInfo;
+ }
+ }
+
+ private static final ProtocolSerializer TEST_SERIALIZER = new ProtocolSerializer<>()
+ {
+ @Override
+ public int getVersion()
+ {
+ return 0x02;
+ }
+
+ @Override
+ public TestProtocol read(InputStream source) throws IOException
+ {
+ return TestProtocol.from(source);
+ }
+
+ @Override
+ public Class getType()
+ {
+ return TestProtocol.class;
+ }
+
+ @Override
+ public byte[] write(TestProtocol protocol)
+ {
+ return protocol.getCanonicalHeader();
+ }
+ };
+
+ @ParameterizedTest
+ @MethodSource("forTestModifiedMessages")
+ void testModifiedMessages(ProtocolAndKeyPair protocolAndKeyPair) throws Exception
+ {
+ ProtocolFactory protocolFactory = new ProtocolFactory(PRE_SHARED_KEY_PROVIDER,
+ ReceiverPrivateKeyProvider.of(RECEIVER_KEY_ID, protocolAndKeyPair.keyPair().getPrivate()),
+ List.of(ProtocolFactory.V1_SERIALIZER, TEST_SERIALIZER))
+ {
+ };
+ Hpke hpke = new Hpke(protocolFactory);
+
+ ZeroInputStream source = new ZeroInputStream(protocolAndKeyPair.protocol().getChunkLength().getLength() + 1);
+ byte[] encrypted = hpke.encrypt(protocolAndKeyPair.protocol(), source, protocolAndKeyPair.keyPair().getPublic())
+ .readAllBytes();
+
+ logger.debug("{}", protocolAndKeyPair.toString());
+
+ assertDoesNotThrow(() -> hpke.decrypt(new ByteArrayInputStream(encrypted), OutputStream.nullOutputStream()));
+
+ AtomicInteger index = new AtomicInteger();
+
+ // Magic
+ byte[] m0 = encrypted.clone();
+ m0[index.get()] = (byte) (m0[index.get()] ^ (byte) 0x01);
+ expectException(hpke, m0, IOException.class);
+ index.getAndUpdate(i -> i += 5);
+
+ // Version
+ byte[] m5Invalid = encrypted.clone();
+ m5Invalid[index.get()] = (byte) 0xFF;
+ expectException(hpke, m5Invalid, IOException.class);
+ byte[] m5Version2 = encrypted.clone();
+ m5Version2[index.get()] = (byte) 0x02;
+ expectException(hpke, m5Version2, IOException.class);
+ index.getAndUpdate(i -> i += 1);
+
+ // Mode
+ byte[] m6invalid = encrypted.clone();
+ m6invalid[index.get()] = (byte) 0xFF;
+ expectException(hpke, m6invalid, IOException.class);
+ byte[] m6other = encrypted.clone();
+ m6other[index
+ .get()] = (byte) (protocolAndKeyPair.protocol().getMode().isPsk() ? Mode.BASE_VALUE : Mode.PSK_VALUE);
+ expectException(hpke, m6other, IOException.class, KeyNotFoundException.class, DecapsulateException.class);
+ index.getAndUpdate(i -> i += 1);
+
+ // KemId
+ byte[] m7invalid = encrypted.clone();
+ m7invalid[index.get()] = (byte) 0xFE;
+ m7invalid[index.get() + 1] = (byte) 0xFF;
+ expectException(hpke, m7invalid, IOException.class);
+ EnumSet.complementOf(EnumSet.of(protocolAndKeyPair.protocol().getKemId())).stream()
+ .map(KemId::getIdAsI2osp2Bytes).forEach(other ->
+ {
+ byte[] m7other = encrypted.clone();
+ m7other[index.get()] = other[0];
+ m7other[index.get() + 1] = other[1];
+ expectException(hpke, m7other, KeyNotSupportedException.class);
+ });
+ index.getAndUpdate(i -> i += 2);
+
+ // KdfId
+ byte[] m9invalid = encrypted.clone();
+ m9invalid[index.get()] = (byte) 0xFF;
+ m9invalid[index.get() + 1] = (byte) 0xFF;
+ expectException(hpke, m9invalid, IOException.class);
+ EnumSet.complementOf(EnumSet.of(protocolAndKeyPair.protocol().getKdfId())).stream()
+ .map(KdfId::getIdAsI2osp2Bytes).forEach(other ->
+ {
+ byte[] m9other = encrypted.clone();
+ m9other[index.get()] = other[0];
+ m9other[index.get() + 1] = other[1];
+ expectException(hpke, m9other, IOException.class);
+ });
+ index.getAndUpdate(i -> i += 2);
+
+ // AeadId
+ byte[] m11invalid = encrypted.clone();
+ m11invalid[index.get()] = (byte) 0xFF;
+ m11invalid[index.get() + 1] = (byte) 0xFF;
+ expectException(hpke, m11invalid, IOException.class);
+ EnumSet.complementOf(EnumSet.of(protocolAndKeyPair.protocol().getAeadId())).stream()
+ .map(AeadId::getIdAsI2osp2Bytes).forEach(other ->
+ {
+ byte[] m11other = encrypted.clone();
+ m11other[index.get()] = other[0];
+ m11other[index.get() + 1] = other[1];
+ expectException(hpke, m11other, IOException.class);
+ });
+ index.getAndUpdate(i -> i += 2);
+
+ // Chunk length
+ byte[] m13invalid = encrypted.clone();
+ m13invalid[index.get()] = (byte) 0xFF;
+ expectException(hpke, m13invalid, IOException.class);
+ EnumSet.complementOf(EnumSet.of(protocolAndKeyPair.protocol().getChunkLength())).stream()
+ .map(ChunkLength::getExponentAsI2osp1Byte).forEach(other ->
+ {
+ byte[] m11other = encrypted.clone();
+ m11other[index.get()] = other[0];
+ expectException(hpke, m11other, IOException.class);
+ });
+ index.getAndUpdate(i -> i += 1);
+
+ // Receiver key ID
+ byte[] m14 = encrypted.clone();
+ m14[index.get()] = (byte) (m14[index.get()] ^ (byte) 0x01);
+ expectException(hpke, m14, KeyNotFoundException.class);
+ index.getAndUpdate(i -> i += ProtocolV1.RECEIVER_KEY_ID_LENGTH);
+
+ // Pre shared key
+ if (protocolAndKeyPair.protocol().getMode().isPsk())
+ {
+ byte[] m46 = encrypted.clone();
+ m46[index.get()] = (byte) (m46[index.get()] ^ (byte) 0x01);
+ expectException(hpke, m46, KeyNotFoundException.class);
+ index.getAndUpdate(i -> i += ProtocolV1.PRE_SHARED_KEY_ID_LENGTH);
+ }
+
+ // Encapsulation
+ byte[] mEnc = encrypted.clone();
+ mEnc[index.get()] = (byte) (mEnc[index.get()] ^ (byte) 0x01);
+ expectException(hpke, mEnc, IOException.class, DecapsulateException.class);
+ index.getAndUpdate(i -> i += protocolAndKeyPair.protocol().getKemId().getEncapsulationLength());
+
+ // First Chunk
+ byte[] mC0 = encrypted.clone();
+ mC0[index.get()] = (byte) (mC0[index.get()] ^ (byte) 0x01);
+ expectException(hpke, mC0, IOException.class);
+ }
+
+ private void expectException(Hpke hpke, byte[] modified, Class>... expected)
+ {
+ try
+ {
+ hpke.decrypt(new ByteArrayInputStream(modified), OutputStream.nullOutputStream());
+ fail("Exception expected");
+ }
+ catch (IOException | GeneralSecurityException | KeyNotFoundException | KeyNotSupportedException e)
+ {
+ if (!List.of(expected).contains(e.getClass()))
+ fail("Exception of type " + e.getClass().getName() + " (message: " + e.getMessage() + ") not expected");
+ }
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KdfIdTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KdfIdTest.java
new file mode 100644
index 0000000..fbd34ef
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KdfIdTest.java
@@ -0,0 +1,67 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.util.stream.Stream;
+
+import javax.crypto.KDF;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class KdfIdTest
+{
+ private static Stream forTestFrom()
+ {
+ return Stream.of(Arguments.of(KdfId.HKDF_SHA256, 0x0001), Arguments.of(KdfId.HKDF_SHA384, 0x0002),
+ Arguments.of(KdfId.HKDF_SHA512, 0x0003));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestFrom")
+ void testFrom(KdfId expected, int id) throws Exception
+ {
+ assertEquals(expected, KdfId.from(new byte[] { (byte) (id >>> 8), (byte) id }));
+ }
+
+ private static Stream forTestFromInvalid()
+ {
+ return Stream.of(Arguments.of(null, NullPointerException.class, "value"),
+ Arguments.of(new byte[0], IllegalArgumentException.class, "value.length not 2"),
+ Arguments.of(new byte[1], IllegalArgumentException.class, "value.length not 2"),
+ Arguments.of(new byte[2], IllegalArgumentException.class, "KdfId not supported"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestFromInvalid")
+ void testFromInvalid(byte[] invalid, Class extends Exception> exceptionClass, String exceptionMessage)
+ throws Exception
+ {
+ Exception exception = assertThrowsExactly(exceptionClass, () -> KdfId.from(invalid));
+ assertEquals(exceptionMessage, exception.getMessage());
+ }
+
+ private static Stream forTestGetter()
+ {
+ return Stream.of(Arguments.of(0x0001, "HKDF-SHA256", KdfId.HKDF_SHA256),
+ Arguments.of(0x0002, "HKDF-SHA384", KdfId.HKDF_SHA384),
+ Arguments.of(0x0003, "HKDF-SHA512", KdfId.HKDF_SHA512));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestGetter")
+ void testGetter(int expectedId, String expectedAlgorithm, KdfId kdfId) throws Exception
+ {
+ assertEquals(expectedId, kdfId.getId());
+ assertArrayEquals(new byte[] { (byte) (expectedId >>> 8), (byte) expectedId }, kdfId.getIdAsI2osp2Bytes());
+ assertEquals(expectedAlgorithm, kdfId.getAlgorithm());
+
+ KDF kdf = kdfId.toKdf();
+ assertNotNull(kdf);
+ assertEquals(expectedAlgorithm, kdf.getAlgorithm());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KemIdTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KemIdTest.java
new file mode 100644
index 0000000..dede8a1
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KemIdTest.java
@@ -0,0 +1,121 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.security.KeyPair;
+import java.util.EnumSet;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory;
+
+public class KemIdTest
+{
+ private static Stream forTestIsKeySupported()
+ {
+ return Stream.of(
+ Arguments.of(KemId.DHKEM_P256_HKDF_SHA256,
+ KeyPairGeneratorFactory.secp256r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_P384_HKDF_SHA384,
+ KeyPairGeneratorFactory.secp384r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_P521_HKDF_SHA512,
+ KeyPairGeneratorFactory.secp521r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_X25519_HKDF_SHA256,
+ KeyPairGeneratorFactory.x25519().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_X448_HKDF_SHA512,
+ KeyPairGeneratorFactory.x448().initialize().generateKeyPair()),
+ Arguments.of(KemId.RSAKEM_1024_KDF2_SHA256,
+ KeyPairGeneratorFactory.rsa1024().initialize().generateKeyPair()),
+ Arguments.of(KemId.RSAKEM_2048_KDF2_SHA256,
+ KeyPairGeneratorFactory.rsa2048().initialize().generateKeyPair()),
+ Arguments.of(KemId.RSAKEM_3072_KDF2_SHA512,
+ KeyPairGeneratorFactory.rsa3072().initialize().generateKeyPair()),
+ Arguments.of(KemId.RSAKEM_4096_KDF2_SHA512,
+ KeyPairGeneratorFactory.rsa4096().initialize().generateKeyPair()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestIsKeySupported")
+ void testIsKeySupported(KemId positive, KeyPair pair) throws Exception
+ {
+ assertTrue(positive.isKeySupported(pair.getPrivate()));
+ assertTrue(positive.isKeySupported(pair.getPublic()));
+
+ EnumSet.complementOf(EnumSet.of(positive)).forEach(n ->
+ {
+ assertFalse(n.isKeySupported(pair.getPrivate()), n.name());
+ assertFalse(n.isKeySupported(pair.getPublic()), n.name());
+ });
+ }
+
+ private static Stream forTestFrom()
+ {
+ return Stream.of(Arguments.of(KemId.DHKEM_P256_HKDF_SHA256, 0x0010),
+ Arguments.of(KemId.DHKEM_P384_HKDF_SHA384, 0x0011), Arguments.of(KemId.DHKEM_P521_HKDF_SHA512, 0x0012),
+ Arguments.of(KemId.DHKEM_X25519_HKDF_SHA256, 0x0020),
+ Arguments.of(KemId.DHKEM_X448_HKDF_SHA512, 0x0021), Arguments.of(KemId.RSAKEM_1024_KDF2_SHA256, 0xFF10),
+ Arguments.of(KemId.RSAKEM_2048_KDF2_SHA256, 0xFF11),
+ Arguments.of(KemId.RSAKEM_3072_KDF2_SHA512, 0xFF12),
+ Arguments.of(KemId.RSAKEM_4096_KDF2_SHA512, 0xFF13));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestFrom")
+ void testFrom(KemId expected, int id) throws Exception
+ {
+ assertEquals(expected, KemId.from(new byte[] { (byte) (id >>> 8), (byte) id }));
+ }
+
+ private static Stream forTestFromInvalid()
+ {
+ return Stream.of(Arguments.of(null, NullPointerException.class, "value"),
+ Arguments.of(new byte[0], IllegalArgumentException.class, "value.length not 2"),
+ Arguments.of(new byte[1], IllegalArgumentException.class, "value.length not 2"),
+ Arguments.of(new byte[2], IllegalArgumentException.class, "KemId not supported"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestFromInvalid")
+ void testFromInvalid(byte[] invalid, Class extends Exception> exceptionClass, String exceptionMessage)
+ throws Exception
+ {
+ Exception exception = assertThrowsExactly(exceptionClass, () -> KemId.from(invalid));
+ assertEquals(exceptionMessage, exception.getMessage());
+ }
+
+ private static Stream forTestGetter()
+ {
+ return Stream.of(Arguments.of(0x0010, 32, 65, DhKemWrapper.class, KemId.DHKEM_P256_HKDF_SHA256),
+ Arguments.of(0x0011, 48, 97, DhKemWrapper.class, KemId.DHKEM_P384_HKDF_SHA384),
+ Arguments.of(0x0012, 64, 133, DhKemWrapper.class, KemId.DHKEM_P521_HKDF_SHA512),
+ Arguments.of(0x0020, 32, 32, DhKemWrapper.class, KemId.DHKEM_X25519_HKDF_SHA256),
+ Arguments.of(0x0021, 64, 56, DhKemWrapper.class, KemId.DHKEM_X448_HKDF_SHA512),
+ Arguments.of(0xFF10, 32, 128, RsaKemWrapper.class, KemId.RSAKEM_1024_KDF2_SHA256),
+ Arguments.of(0xFF11, 32, 256, RsaKemWrapper.class, KemId.RSAKEM_2048_KDF2_SHA256),
+ Arguments.of(0xFF12, 64, 384, RsaKemWrapper.class, KemId.RSAKEM_3072_KDF2_SHA512),
+ Arguments.of(0xFF13, 64, 512, RsaKemWrapper.class, KemId.RSAKEM_4096_KDF2_SHA512));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestGetter")
+ void testGetter(int expectedId, int expectedSharedSecretLength, int expectedEncapsulationLength,
+ Class extends KemWrapper> expectedKemWrapperClass, KemId kemId) throws Exception
+ {
+ assertEquals(expectedId, kemId.getId());
+ assertArrayEquals(new byte[] { (byte) (expectedId >>> 8), (byte) expectedId }, kemId.getIdAsI2osp2Bytes());
+ assertEquals(expectedSharedSecretLength, kemId.getSharedSecretLength());
+ assertEquals(expectedEncapsulationLength, kemId.getEncapsulationLength());
+
+ KemWrapper kem = kemId.toKem();
+ assertNotNull(kem);
+ assertEquals(expectedKemWrapperClass, kem.getClass());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KemWrapperTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KemWrapperTest.java
new file mode 100644
index 0000000..4b71715
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KemWrapperTest.java
@@ -0,0 +1,71 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.security.KeyPair;
+import java.security.SecureRandom;
+import java.util.stream.Stream;
+
+import javax.crypto.KEM.Encapsulated;
+import javax.crypto.SecretKey;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory;
+
+public class KemWrapperTest
+{
+ private static final SecureRandom SECURE_RANDDOM = new SecureRandom();
+
+ private static Stream forTestKem()
+ {
+ return Stream.of(
+ Arguments.of(KemId.DHKEM_P256_HKDF_SHA256,
+ KeyPairGeneratorFactory.secp256r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_P384_HKDF_SHA384,
+ KeyPairGeneratorFactory.secp384r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_P521_HKDF_SHA512,
+ KeyPairGeneratorFactory.secp521r1().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_X25519_HKDF_SHA256,
+ KeyPairGeneratorFactory.x25519().initialize().generateKeyPair()),
+ Arguments.of(KemId.DHKEM_X448_HKDF_SHA512,
+ KeyPairGeneratorFactory.x448().initialize().generateKeyPair()),
+ Arguments.of(KemId.RSAKEM_1024_KDF2_SHA256,
+ KeyPairGeneratorFactory.rsa1024().initialize().generateKeyPair()),
+ Arguments.of(KemId.RSAKEM_2048_KDF2_SHA256,
+ KeyPairGeneratorFactory.rsa2048().initialize().generateKeyPair()),
+ Arguments.of(KemId.RSAKEM_3072_KDF2_SHA512,
+ KeyPairGeneratorFactory.rsa3072().initialize().generateKeyPair()),
+ Arguments.of(KemId.RSAKEM_4096_KDF2_SHA512,
+ KeyPairGeneratorFactory.rsa4096().initialize().generateKeyPair()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestKem")
+ void testKem(KemId kemId, KeyPair keyPair) throws Exception
+ {
+ KemWrapper kem = kemId.toKem();
+ assertNotNull(kem);
+
+ Encapsulated encapsulated = kem.getEncapsulated(keyPair.getPublic(), SECURE_RANDDOM);
+ assertNotNull(encapsulated);
+
+ byte[] encapsulation = encapsulated.encapsulation();
+ assertNotNull(encapsulation);
+ assertEquals(kemId.getEncapsulationLength(), encapsulation.length);
+
+ SecretKey sharedSecretSender = encapsulated.key();
+ assertNotNull(sharedSecretSender);
+ assertEquals(kemId.getSharedSecretLength(), sharedSecretSender.getEncoded().length);
+
+ SecretKey sharedSecretReceiver = kem.getSharedSecret(keyPair.getPrivate(), encapsulation);
+ assertNotNull(sharedSecretReceiver);
+ assertEquals(kemId.getSharedSecretLength(), sharedSecretReceiver.getEncoded().length);
+
+ assertArrayEquals(sharedSecretSender.getEncoded(), sharedSecretReceiver.getEncoded());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KeyProviderTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KeyProviderTest.java
new file mode 100644
index 0000000..f2ba37f
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KeyProviderTest.java
@@ -0,0 +1,110 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.nio.charset.StandardCharsets;
+import java.security.Key;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory;
+
+public class KeyProviderTest
+{
+ private static final byte[] PSK_ID_1 = Sha256
+ .digest("Test Pre Shared Key ID 1".getBytes(StandardCharsets.US_ASCII));
+ private static final byte[] PSK_ID_2 = Sha256
+ .digest("Test Pre Shared Key ID 2".getBytes(StandardCharsets.US_ASCII));
+ private static final SecretKey PSK_1 = new SecretKeySpec(
+ new byte[] { 'T', 'e', 's', 't', ' ', 'P', 'S', 'K', ' ', '1' }, "Generic");
+ private static final SecretKey PSK_2 = new SecretKeySpec(
+ new byte[] { 'T', 'e', 's', 't', ' ', 'P', 'S', 'K', ' ', '2' }, "Generic");
+
+ private static final KeyPairGenerator RK_GENERATOR = KeyPairGeneratorFactory.rsa1024().initialize();
+
+ private static final byte[] RK_ID_1 = Sha256.digest("Test Receiver Key ID 1".getBytes(StandardCharsets.US_ASCII));
+ private static final PrivateKey RK_1 = RK_GENERATOR.generateKeyPair().getPrivate();
+ private static final byte[] RK_ID_2 = Sha256.digest("Test Receiver Key ID 2".getBytes(StandardCharsets.US_ASCII));
+ private static final PrivateKey RK_2 = RK_GENERATOR.generateKeyPair().getPrivate();
+
+ private static Stream forTestOf()
+ {
+ return Stream.of(Arguments.of(PreSharedKeyProvider.of(), KeyProvider.PSK, PSK_ID_1, PSK_ID_2),
+ Arguments.of(ReceiverPrivateKeyProvider.of(), KeyProvider.RECEIVER_KEY_ID, RK_ID_1, RK_ID_2));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestOf")
+ void testOf(KeyProvider provider, String type, byte[] id1, byte[] id2) throws Exception
+ {
+ testNotFound(provider, type, id1);
+ testNotFound(provider, type, id2);
+ testNotFound(provider, type, new byte[0]);
+ testNotFound(provider, type, null);
+ }
+
+ private static Stream forTestOf1()
+ {
+ return Stream.of(
+ Arguments.of(PreSharedKeyProvider.of(PSK_ID_1, PSK_1), KeyProvider.PSK, PSK_ID_1, PSK_1, PSK_ID_2),
+ Arguments.of(ReceiverPrivateKeyProvider.of(RK_ID_1, RK_1), KeyProvider.RECEIVER_KEY_ID, RK_ID_1, RK_1,
+ RK_ID_2));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestOf1")
+ void testOf1(KeyProvider provider, String type, byte[] id1, K key1, byte[] id2) throws Exception
+ {
+ testFound(provider, id1, key1);
+
+ testNotFound(provider, type, id2);
+ testNotFound(provider, type, new byte[0]);
+ testNotFound(provider, type, null);
+ }
+
+ private static Stream forTestOf2()
+ {
+ return Stream.of(
+ Arguments.of(PreSharedKeyProvider.of(Map.of(PSK_ID_1, PSK_1, PSK_ID_2, PSK_2)), KeyProvider.PSK,
+ PSK_ID_1, PSK_1, PSK_ID_2, PSK_2),
+ Arguments.of(ReceiverPrivateKeyProvider.of(Map.of(RK_ID_1, RK_1, RK_ID_2, RK_2)),
+ KeyProvider.RECEIVER_KEY_ID, RK_ID_1, RK_1, RK_ID_2, RK_2));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestOf2")
+ void testOf2(KeyProvider provider, String type, byte[] id1, K key1, byte[] id2, K key2)
+ throws Exception
+ {
+ testFound(provider, id1, key1);
+ testFound(provider, id2, key2);
+
+ testNotFound(provider, type, new byte[0]);
+ testNotFound(provider, type, null);
+ }
+
+ private void testFound(KeyProvider provider, byte[] pskId, K expected)
+ throws KeyNotFoundException
+ {
+ Key k = provider.retrieve(pskId);
+ assertNotNull(k);
+ assertEquals(expected, k);
+ }
+
+ private void testNotFound(KeyProvider> provider, String type, byte[] pskId)
+ {
+ KeyNotFoundException e = assertThrowsExactly(KeyNotFoundException.class, () -> provider.retrieve(pskId));
+ assertEquals(KeyProvider.notFound(type, pskId).getMessage(), e.getMessage());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KeyScheduleTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KeyScheduleTest.java
new file mode 100644
index 0000000..6806ba9
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/KeyScheduleTest.java
@@ -0,0 +1,232 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.math.BigInteger;
+import java.security.AlgorithmParameters;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPrivateKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.NamedParameterSpec;
+import java.security.spec.XECPrivateKeySpec;
+import java.util.HexFormat;
+import java.util.stream.Stream;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.hsheilbronn.mi.utils.crypto.hpke.KeySchedule.Result;
+
+public class KeyScheduleTest
+{
+ private static record Rfc9180TestData(String name, Mode mode, KemId kemId, KdfId kdfId, AeadId aeadId,
+ PrivateKey skRm, byte[] enc, byte[] sharedSecret, byte[] key, byte[] baseNonce)
+ {
+ public static Rfc9180TestData withX25519(String name, Mode mode, KemId kemId, KdfId kdfId, AeadId aeadId,
+ String skRm, String enc, String sharedSecret, String key, String baseNonce)
+ {
+ return new Rfc9180TestData(name, mode, kemId, kdfId, aeadId, toX25519PrivateKey(skRm),
+ HexFormat.of().parseHex(enc), HexFormat.of().parseHex(sharedSecret), HexFormat.of().parseHex(key),
+ HexFormat.of().parseHex(baseNonce));
+ }
+
+ private static PrivateKey toX25519PrivateKey(String hex)
+ {
+ try
+ {
+ XECPrivateKeySpec spec = new XECPrivateKeySpec(NamedParameterSpec.X25519, HexFormat.of().parseHex(hex));
+ return KeyFactory.getInstance("X25519").generatePrivate(spec);
+ }
+ catch (InvalidKeySpecException | NoSuchAlgorithmException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Rfc9180TestData withSecp256r1(String name, Mode mode, KemId kemId, KdfId kdfId, AeadId aeadId,
+ String skRm, String enc, String sharedSecret, String key, String baseNonce)
+ {
+ return new Rfc9180TestData(name, mode, kemId, kdfId, aeadId, toEcPrivateKey("secp256r1", skRm),
+ HexFormat.of().parseHex(enc), HexFormat.of().parseHex(sharedSecret), HexFormat.of().parseHex(key),
+ HexFormat.of().parseHex(baseNonce));
+ }
+
+ public static Rfc9180TestData withSecp521r1(String name, Mode mode, KemId kemId, KdfId kdfId, AeadId aeadId,
+ String skRm, String enc, String sharedSecret, String key, String baseNonce)
+ {
+ return new Rfc9180TestData(name, mode, kemId, kdfId, aeadId, toEcPrivateKey("secp521r1", skRm),
+ HexFormat.of().parseHex(enc), HexFormat.of().parseHex(sharedSecret), HexFormat.of().parseHex(key),
+ HexFormat.of().parseHex(baseNonce));
+ }
+
+ private static PrivateKey toEcPrivateKey(String curve, String hex)
+ {
+ try
+ {
+ AlgorithmParameters params = AlgorithmParameters.getInstance("EC");
+ params.init(new ECGenParameterSpec(curve));
+ ECParameterSpec ecParams = params.getParameterSpec(ECParameterSpec.class);
+ ECPrivateKeySpec spec = new ECPrivateKeySpec(new BigInteger(1, HexFormat.of().parseHex(hex)), ecParams);
+ return KeyFactory.getInstance("EC").generatePrivate(spec);
+ }
+ catch (InvalidKeySpecException | NoSuchAlgorithmException | InvalidParameterSpecException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public final String toString()
+ {
+ return name;
+ }
+ }
+
+ // Test Data from https://www.rfc-editor.org/rfc/rfc9180.html#name-test-vectors
+
+ private static final byte[] INFO = HexFormat.of().parseHex("4f6465206f6e2061204772656369616e2055726e");
+
+ private static final byte[] PSK_ID = HexFormat.of().parseHex("456e6e796e20447572696e206172616e204d6f726961");
+ private static final SecretKey PSK = new SecretKeySpec(
+ HexFormat.of().parseHex("0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"), "Generic");
+ private static final PreSharedKeyProvider PSK_PROVIDER = PreSharedKeyProvider.of(PSK_ID, PSK);
+
+ private static final Rfc9180TestData A11 = Rfc9180TestData.withX25519("A.1.1", Mode.base(),
+ KemId.DHKEM_X25519_HKDF_SHA256, KdfId.HKDF_SHA256, AeadId.AES_128_GCM,
+ "4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8",
+ "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431",
+ "fe0e18c9f024ce43799ae393c7e8fe8fce9d218875e8227b0187c04e7d2ea1fc", //
+ "4531685d41d65f03dc48f6b8302c05b0", "56d890e5accaaf011cff4b7d");
+ private static final Rfc9180TestData A12 = Rfc9180TestData.withX25519("A.1.2", Mode.psk(PSK_ID),
+ KemId.DHKEM_X25519_HKDF_SHA256, KdfId.HKDF_SHA256, AeadId.AES_128_GCM,
+ "c5eb01eb457fe6c6f57577c5413b931550a162c71a03ac8d196babbd4e5ce0fd",
+ "0ad0950d9fb9588e59690b74f1237ecdf1d775cd60be2eca57af5a4b0471c91b",
+ "727699f009ffe3c076315019c69648366b69171439bd7dd0807743bde76986cd", //
+ "15026dba546e3ae05836fc7de5a7bb26", "9518635eba129d5ce0914555");
+
+ private static final Rfc9180TestData A21 = Rfc9180TestData.withX25519("A.2.1", Mode.base(),
+ KemId.DHKEM_X25519_HKDF_SHA256, KdfId.HKDF_SHA256, AeadId.ChaCha20Poly1305,
+ "8057991eef8f1f1af18f4a9491d16a1ce333f695d4db8e38da75975c4478e0fb",
+ "1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a",
+ "0bbe78490412b4bbea4812666f7916932b828bba79942424abb65244930d69a7",
+ "ad2744de8e17f4ebba575b3f5f5a8fa1f69c2a07f6e7500bc60ca6e3e3ec1c91", "5c4d98150661b848853b547f");
+ private static final Rfc9180TestData A22 = Rfc9180TestData.withX25519("A.2.2", Mode.psk(PSK_ID),
+ KemId.DHKEM_X25519_HKDF_SHA256, KdfId.HKDF_SHA256, AeadId.ChaCha20Poly1305,
+ "77d114e0212be51cb1d76fa99dd41cfd4d0166b08caa09074430a6c59ef17879",
+ "2261299c3f40a9afc133b969a97f05e95be2c514e54f3de26cbe5644ac735b04",
+ "4be079c5e77779d0215b3f689595d59e3e9b0455d55662d1f3666ec606e50ea7",
+ "600d2fdb0313a7e5c86a9ce9221cd95bed069862421744cfb4ab9d7203a9c019", "112e0465562045b7368653e7");
+
+ private static final Rfc9180TestData A31 = Rfc9180TestData.withSecp256r1("A.3.1", Mode.base(),
+ KemId.DHKEM_P256_HKDF_SHA256, KdfId.HKDF_SHA256, AeadId.AES_128_GCM,
+ "f3ce7fdae57e1a310d87f1ebbde6f328be0a99cdbcadf4d6589cf29de4b8ffd2",
+ "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b325"
+ + "ac98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4",
+ "c0d26aeab536609a572b07695d933b589dcf363ff9d93c93adea537aeabb8cb8", //
+ "868c066ef58aae6dc589b6cfdd18f97e", "4e0bc5018beba4bf004cca59");
+ private static final Rfc9180TestData A32 = Rfc9180TestData.withSecp256r1("A.3.2", Mode.psk(PSK_ID),
+ KemId.DHKEM_P256_HKDF_SHA256, KdfId.HKDF_SHA256, AeadId.AES_128_GCM,
+ "438d8bcef33b89e0e9ae5eb0957c353c25a94584b0dd59c991372a75b43cb661",
+ "04305d35563527bce037773d79a13deabed0e8e7cde61eecee403496959e89e"
+ + "4d0ca701726696d1485137ccb5341b3c1c7aaee90a4a02449725e744b1193b53b5f",
+ "2e783ad86a1beae03b5749e0f3f5e9bb19cb7eb382f2fb2dd64c99f15ae0661b", //
+ "55d9eb9d26911d4c514a990fa8d57048", "b595dc6b2d7e2ed23af529b1");
+
+ private static final Rfc9180TestData A41 = Rfc9180TestData.withSecp256r1("A.4.1", Mode.base(),
+ KemId.DHKEM_P256_HKDF_SHA256, KdfId.HKDF_SHA512, AeadId.AES_128_GCM,
+ "3ac8530ad1b01885960fab38cf3cdc4f7aef121eaa239f222623614b4079fb38",
+ "0493ed86735bdfb978cc055c98b45695ad7ce61ce748f4dd63c525a3b8d53a1"
+ + "5565c6897888070070c1579db1f86aaa56deb8297e64db7e8924e72866f9a472580",
+ "02f584736390fc93f5b4ad039826a3fa08e9911bd1215a3db8e8791ba533cafd", //
+ "090ca96e5f8aa02b69fac360da50ddf9", "9c995e621bf9a20c5ca45546");
+ private static final Rfc9180TestData A42 = Rfc9180TestData.withSecp256r1("A.4.2", Mode.psk(PSK_ID),
+ KemId.DHKEM_P256_HKDF_SHA256, KdfId.HKDF_SHA512, AeadId.AES_128_GCM,
+ "bc6f0b5e22429e5ff47d5969003f3cae0f4fec50e23602e880038364f33b8522",
+ "04a307934180ad5287f95525fe5bc6244285d7273c15e061f0f2efb211c3505"
+ + "7f3079f6e0abae200992610b25f48b63aacfcb669106ddee8aa023feed301901371",
+ "2912aacc6eaebd71ff715ea50f6ef3a6637856b2a4c58ea61e0c3fc159e3bc16", //
+ "0b910ba8d9cfa17e5f50c211cb32839a", "0c29e714eb52de5b7415a1b7");
+
+ private static final Rfc9180TestData A51 = Rfc9180TestData.withSecp256r1("A.5.1", Mode.base(),
+ KemId.DHKEM_P256_HKDF_SHA256, KdfId.HKDF_SHA256, AeadId.ChaCha20Poly1305,
+ "a4d1c55836aa30f9b3fbb6ac98d338c877c2867dd3a77396d13f68d3ab150d3b",
+ "04c07836a0206e04e31d8ae99bfd549380b072a1b1b82e563c935c095827824"
+ + "fc1559eac6fb9e3c70cd3193968994e7fe9781aa103f5b50e934b5b2f387e381291",
+ "806520f82ef0b03c823b7fc524b6b55a088f566b9751b89551c170f4113bd850",
+ "a8f45490a92a3b04d1dbf6cf2c3939ad8bfc9bfcb97c04bffe116730c9dfe3fc", "726b4390ed2209809f58c693");
+ private static final Rfc9180TestData A52 = Rfc9180TestData.withSecp256r1("A.5.2", Mode.psk(PSK_ID),
+ KemId.DHKEM_P256_HKDF_SHA256, KdfId.HKDF_SHA256, AeadId.ChaCha20Poly1305,
+ "12ecde2c8bc2d5d7ed2219c71f27e3943d92b344174436af833337c557c300b3",
+ "04f336578b72ad7932fe867cc4d2d44a718a318037a0ec271163699cee653fa"
+ + "805c1fec955e562663e0c2061bb96a87d78892bff0cc0bad7906c2d998ebe1a7246",
+ "ac4f260dce4db6bf45435d9c92c0e11cfdd93743bd3075949975974cc2b3d79e",
+ "6d61cb330b7771168c8619498e753f16198aad9566d1f1c6c70e2bc1a1a8b142", "0de7655fb65e1cd51a38864e");
+
+ private static final Rfc9180TestData A61 = Rfc9180TestData.withSecp521r1("A.6.1", Mode.base(),
+ KemId.DHKEM_P521_HKDF_SHA512, KdfId.HKDF_SHA512, AeadId.AES_256_GCM,
+ "01462680369ae375e4b3791070a7458ed527842f6a98a79ff5e0d4cbde83c2"
+ + "7196a3916956655523a6a2556a7af62c5cadabe2ef9da3760bb21e005202f7b24628" //
+ + "47",
+ "040138b385ca16bb0d5fa0c0665fbbd7e69e3ee29f63991d3e9b5fa740aab89"
+ + "00aaeed46ed73a49055758425a0ce36507c54b29cc5b85a5cee6bae0cf1c21f2731e"
+ + "ce2013dc3fb7c8d21654bb161b463962ca19e8c654ff24c94dd2898de12051f1ed06"
+ + "92237fb02b2f8d1dc1c73e9b366b529eb436e98a996ee522aef863dd5739d2f29b0",
+ "776ab421302f6eff7d7cb5cb1adaea0cd50872c71c2d63c30c4f1"
+ + "d5e43653336fef33b103c67e7a98add2d3b66e2fda95b5b2a667aa9dac7e59cc1d46" //
+ + "d30e818",
+ "751e346ce8f0ddb2305c8a2a85c70d5cf559c53093656be636b9406d4d7d1b70", "55ff7a7d739c69f44b25447b");
+ private static final Rfc9180TestData A62 = Rfc9180TestData.withSecp521r1("A.6.2", Mode.psk(PSK_ID),
+ KemId.DHKEM_P521_HKDF_SHA512, KdfId.HKDF_SHA512, AeadId.AES_256_GCM,
+ "011bafd9c7a52e3e71afbdab0d2f31b03d998a0dc875dd7555c63560e142bd"
+ + "e264428de03379863b4ec6138f813fa009927dc5d15f62314c56d4e7ff2b485753eb" //
+ + "72",
+ "040085eff0835cc84351f32471d32aa453cdc1f6418eaaecf1c2824210eb1d4"
+ + "8d0768b368110fab21407c324b8bb4bec63f042cfa4d0868d19b760eb4beba1bff79"
+ + "3b30036d2c614d55730bd2a40c718f9466faf4d5f8170d22b6df98dfe0c067d02b34"
+ + "9ae4a142e0c03418f0a1479ff78a3db07ae2c2e89e5840f712c174ba2118e90fdcb",
+ "0d52de997fdaa4797720e8b1bebd3df3d03c4cf38cc8c1398168d"
+ + "36c3fc7626428c9c254dd3f9274450909c64a5b3acbe45e2d850a2fd69ac0605fe5c" //
+ + "8a057a5",
+ "f764a5a4b17e5d1ffba6e699d65560497ebaea6eb0b0d9010a6d979e298a39ff", "479afdf3546ddba3a9841f38");
+
+ private static Stream forTestExecuteKeySchedule()
+ {
+ return Stream.of(Arguments.of(A11), Arguments.of(A12), Arguments.of(A21), Arguments.of(A22), Arguments.of(A31),
+ Arguments.of(A32), Arguments.of(A41), Arguments.of(A42), Arguments.of(A51), Arguments.of(A52),
+ Arguments.of(A61), Arguments.of(A62));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestExecuteKeySchedule")
+ void testExecuteKeySchedule(Rfc9180TestData testData) throws Exception
+ {
+ KemWrapper kem = testData.kemId().toKem();
+ assertNotNull(kem);
+
+ SecretKey sharedSecret = kem.getSharedSecret(testData.skRm(), testData.enc());
+ assertNotNull(sharedSecret);
+ assertArrayEquals(testData.sharedSecret(), sharedSecret.getEncoded());
+
+ KeySchedule keySchedule = new KeySchedule(testData.mode(), testData.kemId(), testData.kdfId(),
+ testData.aeadId(), INFO, PSK_PROVIDER);
+
+ Result result = keySchedule.executeKeySchedule(sharedSecret);
+ assertNotNull(result);
+
+ assertNotNull(result.key());
+ assertNotNull(result.baseNonce());
+
+ assertArrayEquals(testData.key(), result.key().getEncoded());
+ assertArrayEquals(testData.baseNonce(), result.baseNonce());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ModeTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ModeTest.java
new file mode 100644
index 0000000..517f0f0
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ModeTest.java
@@ -0,0 +1,100 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class ModeTest
+{
+ private static final byte[] PSK_ID = Sha256.digest("Test PSK Identifier".getBytes(StandardCharsets.US_ASCII));
+
+ @Test
+ void testPskFactoryMethods() throws Exception
+ {
+ assertDoesNotThrow(() -> Mode.base());
+ assertDoesNotThrow(() -> Mode.psk(PSK_ID));
+ assertThrowsExactly(NullPointerException.class, () -> Mode.psk(null));
+
+ IllegalArgumentException e = assertThrowsExactly(IllegalArgumentException.class, () -> Mode.psk(new byte[0]));
+ assertEquals("pskId.length <= 0", e.getMessage());
+ }
+
+ @Test
+ @SuppressWarnings("unlikely-arg-type") // equals string
+ void testBase() throws Exception
+ {
+ Mode base = Mode.base();
+ assertArrayEquals(new byte[] { (byte) 0x00 }, base.getValueAsI2osp1Byte());
+ assertArrayEquals(new byte[0], base.getPskId());
+
+ assertFalse(base.isPsk());
+
+ assertEquals(base.hashCode(), Mode.base().hashCode());
+ assertTrue(base.equals(base));
+ assertTrue(base.equals(Mode.base()));
+ assertFalse(base.equals(Mode.psk(PSK_ID)));
+
+ assertFalse(base.equals(null));
+ assertFalse(base.equals(""));
+ }
+
+ @Test
+ @SuppressWarnings("unlikely-arg-type") // equals string
+ void testPsk() throws Exception
+ {
+ Mode psk = Mode.psk(PSK_ID);
+ assertArrayEquals(new byte[] { (byte) 0x01 }, psk.getValueAsI2osp1Byte());
+ assertArrayEquals(PSK_ID, psk.getPskId());
+
+ assertTrue(psk.isPsk());
+
+ assertEquals(psk.hashCode(), Mode.psk(PSK_ID).hashCode());
+ assertTrue(psk.equals(psk));
+ assertTrue(psk.equals(Mode.psk(PSK_ID)));
+ assertFalse(psk.equals(Mode.psk(new byte[1])));
+ assertFalse(psk.equals(Mode.base()));
+
+ assertFalse(psk.equals(null));
+ assertFalse(psk.equals(""));
+ }
+
+ private static Stream forTestFrom()
+ {
+ return Stream.of(Arguments.of(Mode.base(), 0x00, null), Arguments.of(Mode.psk(PSK_ID), 0x01, PSK_ID));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestFrom")
+ void testFrom(Mode expected, int id, byte[] pskId) throws Exception
+ {
+ assertEquals(expected, Mode.from((byte) id, pskId));
+ }
+
+ private static Stream forTestFromInvalid()
+ {
+ return Stream.of(Arguments.of(0xFF, PSK_ID, IllegalArgumentException.class, "Mode not supported"),
+ Arguments.of(Mode.PSK_VALUE, null, IllegalArgumentException.class, "Mode not supported"),
+ Arguments.of(Mode.BASE_VALUE, new byte[0], IllegalArgumentException.class, "Mode not supported"),
+ Arguments.of(Mode.PSK_VALUE, new byte[0], IllegalArgumentException.class, "pskId.length <= 0"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestFromInvalid")
+ void testFromInvalid(int invalid, byte[] pskId, Class extends Exception> exceptionClass, String exceptionMessage)
+ throws Exception
+ {
+ Exception exception = assertThrowsExactly(exceptionClass, () -> Mode.from((byte) invalid, pskId));
+ assertEquals(exceptionMessage, exception.getMessage());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolFactoryTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolFactoryTest.java
new file mode 100644
index 0000000..a09e770
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolFactoryTest.java
@@ -0,0 +1,233 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.hsheilbronn.mi.utils.crypto.hpke.ProtocolFactory.ProtocolSerializer;
+
+public class ProtocolFactoryTest
+{
+ private static final class TestProtocol implements Protocol
+ {
+ @Override
+ public Mode getMode()
+ {
+ return null;
+ }
+
+ @Override
+ public KemId getKemId()
+ {
+ return null;
+ }
+
+ @Override
+ public KdfId getKdfId()
+ {
+ return null;
+ }
+
+ @Override
+ public AeadId getAeadId()
+ {
+ return null;
+ }
+
+ @Override
+ public ChunkLength getChunkLength()
+ {
+ return null;
+ }
+
+ @Override
+ public byte[] getKdfInfo()
+ {
+ return null;
+ }
+
+ @Override
+ public byte[] getReceiverKeyId()
+ {
+ return null;
+ }
+ }
+
+ private static final ProtocolSerializer TEST_SERIALIZER = new ProtocolSerializer()
+ {
+ @Override
+ public int getVersion()
+ {
+ return 0xFF;
+ }
+
+ @Override
+ public TestProtocol read(InputStream stream) throws IOException
+ {
+ return new TestProtocol();
+ }
+
+ @Override
+ public Class getType()
+ {
+ return TestProtocol.class;
+ }
+
+ @Override
+ public byte[] write(TestProtocol protocol)
+ {
+ return new byte[0];
+ }
+ };
+
+ private static Stream forTestConstructor()
+ {
+ return Stream.of(
+ Arguments.of((Supplier) () -> new ProtocolFactory(PreSharedKeyProvider.of(),
+ ReceiverPrivateKeyProvider.of()), null, null),
+ Arguments.of((Supplier) () -> new ProtocolFactory(PreSharedKeyProvider.of(),
+ ReceiverPrivateKeyProvider.of(), null), null, null),
+ Arguments.of((Supplier) () -> new ProtocolFactory(PreSharedKeyProvider.of(),
+ ReceiverPrivateKeyProvider.of(), List.of()), null, null),
+ Arguments.of((Supplier) () -> new ProtocolFactory(PreSharedKeyProvider.of(),
+ ReceiverPrivateKeyProvider.of(), List.of(TEST_SERIALIZER)), null, null),
+ Arguments.of((Supplier) () -> new ProtocolFactory(null,
+ ReceiverPrivateKeyProvider.of(), List.of()), NullPointerException.class,
+ "preSharedKeyProvider"),
+ Arguments.of((Supplier) () -> new ProtocolFactory(PreSharedKeyProvider.of(), null,
+ List.of()), NullPointerException.class, "receiverPrivateKeyProvider"),
+ Arguments.of(
+ (Supplier) () -> new ProtocolFactory(PreSharedKeyProvider.of(),
+ ReceiverPrivateKeyProvider.of(), List.of(TEST_SERIALIZER, TEST_SERIALIZER)),
+ IllegalArgumentException.class, "Multiple protocol serializers for same version"));
+ }
+
+ @MethodSource("forTestConstructor")
+ @ParameterizedTest
+ void testConstructor(Supplier constructor, Class extends Exception> expectedException,
+ String expectedExceptionMessage) throws Exception
+ {
+ if (expectedException == null)
+ assertDoesNotThrow(() -> constructor.get());
+ else
+ {
+ Exception e = assertThrowsExactly(expectedException, () -> constructor.get());
+ assertEquals(expectedExceptionMessage, e.getMessage());
+ }
+ }
+
+ @Test
+ void testWriteReadTestProtocol() throws Exception
+ {
+ ProtocolFactory factory = new ProtocolFactory(PreSharedKeyProvider.of(), ReceiverPrivateKeyProvider.of(),
+ List.of(TEST_SERIALIZER))
+ {
+ };
+
+ InputStream stream = factory.write(new TestProtocol());
+ assertNotNull(stream);
+
+ byte[] header = stream.readAllBytes();
+
+ assertEquals(ProtocolFactory.ROOT_HEADER_LENGTH, header.length);
+ assertArrayEquals(ByteEncoding.concat(ProtocolFactory.MAGIC, new byte[] { (byte) 0xFF }), header);
+
+ Protocol read = factory.read(new ByteArrayInputStream(header));
+ assertNotNull(read);
+ assertTrue(read instanceof TestProtocol);
+ }
+
+ @Test
+ void testWriteReadV1Protocol() throws Exception
+ {
+ ProtocolFactory factory = new ProtocolFactory(PreSharedKeyProvider.of(), ReceiverPrivateKeyProvider.of());
+
+ InputStream stream = factory.write(new ProtocolV1(Mode.base(), KemId.DHKEM_P256_HKDF_SHA256, KdfId.HKDF_SHA256,
+ AeadId.AES_128_GCM, ChunkLength.KiB_1, new byte[ProtocolV1.RECEIVER_KEY_ID_LENGTH]));
+ assertNotNull(stream);
+
+ byte[] header = stream.readAllBytes();
+
+ assertEquals(ProtocolFactory.ROOT_HEADER_LENGTH + ProtocolV1.HEADER_BASE_LENGTH, header.length);
+
+ Protocol read = factory.read(new ByteArrayInputStream(header));
+ assertNotNull(read);
+ assertTrue(read instanceof ProtocolV1);
+ }
+
+ @Test
+ void testWriteReadTestAndV1Protocol() throws Exception
+ {
+ ProtocolFactory factory = new ProtocolFactory(PreSharedKeyProvider.of(), ReceiverPrivateKeyProvider.of(),
+ List.of(TEST_SERIALIZER, ProtocolFactory.V1_SERIALIZER))
+ {
+ };
+
+ InputStream streamV1 = factory.write(new ProtocolV1(Mode.base(), KemId.DHKEM_P256_HKDF_SHA256,
+ KdfId.HKDF_SHA256, AeadId.AES_128_GCM, ChunkLength.KiB_1, new byte[ProtocolV1.RECEIVER_KEY_ID_LENGTH]));
+ assertNotNull(streamV1);
+
+ byte[] headerV1 = streamV1.readAllBytes();
+
+ assertEquals(ProtocolFactory.ROOT_HEADER_LENGTH + ProtocolV1.HEADER_BASE_LENGTH, headerV1.length);
+
+ Protocol readV1 = factory.read(new ByteArrayInputStream(headerV1));
+ assertNotNull(readV1);
+ assertTrue(readV1 instanceof ProtocolV1);
+
+ InputStream streamTest = factory.write(new TestProtocol());
+ assertNotNull(streamTest);
+
+ byte[] headerTest = streamTest.readAllBytes();
+
+ assertEquals(ProtocolFactory.ROOT_HEADER_LENGTH, headerTest.length);
+ assertArrayEquals(ByteEncoding.concat(ProtocolFactory.MAGIC, new byte[] { (byte) 0xFF }), headerTest);
+
+ Protocol readTest = factory.read(new ByteArrayInputStream(headerTest));
+ assertNotNull(readTest);
+ assertTrue(readTest instanceof TestProtocol);
+ }
+
+ @Test
+ void testReadWriteNull() throws Exception
+ {
+ ProtocolFactory factory = new ProtocolFactory(PreSharedKeyProvider.of(), ReceiverPrivateKeyProvider.of());
+
+ NullPointerException e = assertThrowsExactly(NullPointerException.class, () -> factory.write(null));
+ assertEquals("protocol", e.getMessage());
+ e = assertThrowsExactly(NullPointerException.class, () -> factory.read(null));
+ assertEquals("source", e.getMessage());
+ }
+
+ @Test
+ void testProtocolNotSupported() throws Exception
+ {
+ ProtocolFactory factory = new ProtocolFactory(PreSharedKeyProvider.of(), ReceiverPrivateKeyProvider.of());
+
+ IllegalArgumentException iaE = assertThrowsExactly(IllegalArgumentException.class,
+ () -> factory.write(new TestProtocol()));
+ assertEquals("Protocol not supported", iaE.getMessage());
+
+ IOException ioE = assertThrowsExactly(IOException.class, () -> factory.read(new ByteArrayInputStream(
+ ByteEncoding.concat(ProtocolFactory.MAGIC, new byte[] { (byte) TEST_SERIALIZER.getVersion() }))));
+ assertEquals("Protocol not supported", ioE.getMessage());
+ ioE = assertThrowsExactly(IOException.class, () -> factory.read(new ByteArrayInputStream(ByteEncoding
+ .concat(new byte[ProtocolFactory.MAGIC.length], new byte[] { (byte) TEST_SERIALIZER.getVersion() }))));
+ assertEquals("Protocol not supported", ioE.getMessage());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolV1Test.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolV1Test.java
new file mode 100644
index 0000000..3a55af8
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ProtocolV1Test.java
@@ -0,0 +1,231 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumSet;
+import java.util.HexFormat;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ProtocolV1Test
+{
+ private static final Logger logger = LoggerFactory.getLogger(ProtocolV1Test.class);
+
+ private static final byte[] PSK_ID = Sha256.digest("Test PSK Identifier".getBytes(StandardCharsets.US_ASCII));
+ private static final byte[] RECEIVER_KEY_IDENTIFIER = Sha256
+ .digest("Test Receiver Key Identifier".getBytes(StandardCharsets.US_ASCII));
+
+ private static final ProtocolV1 V1_BASE = new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256,
+ KdfId.HKDF_SHA256, AeadId.AES_128_GCM, ChunkLength.KiB_1, RECEIVER_KEY_IDENTIFIER);
+ private static final byte[] V1_BASE_BYTE_ARRAY = ByteEncoding.concat(Mode.base().getValueAsI2osp1Byte(),
+ KemId.DHKEM_X25519_HKDF_SHA256.getIdAsI2osp2Bytes(), KdfId.HKDF_SHA256.getIdAsI2osp2Bytes(),
+ AeadId.AES_128_GCM.getIdAsI2osp2Bytes(), ChunkLength.KiB_1.getExponentAsI2osp1Byte(),
+ RECEIVER_KEY_IDENTIFIER);
+
+ private static final ProtocolV1 V1_PSK = new ProtocolV1(Mode.psk(PSK_ID), KemId.DHKEM_P521_HKDF_SHA512,
+ KdfId.HKDF_SHA512, AeadId.ChaCha20Poly1305, ChunkLength.MiB_1, RECEIVER_KEY_IDENTIFIER);
+ private static final byte[] V1_PSK_BYTE_ARRAY = ByteEncoding.concat(Mode.psk(PSK_ID).getValueAsI2osp1Byte(),
+ KemId.DHKEM_P521_HKDF_SHA512.getIdAsI2osp2Bytes(), KdfId.HKDF_SHA512.getIdAsI2osp2Bytes(),
+ AeadId.ChaCha20Poly1305.getIdAsI2osp2Bytes(), ChunkLength.MiB_1.getExponentAsI2osp1Byte(),
+ RECEIVER_KEY_IDENTIFIER, PSK_ID);
+
+ private static Stream forTestConstructorExceptions()
+ {
+ return Stream.of(
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256,
+ KdfId.HKDF_SHA256, AeadId.AES_128_GCM, ChunkLength.KiB_1, RECEIVER_KEY_IDENTIFIER),
+ null, null),
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(Mode.psk(PSK_ID), KemId.DHKEM_P521_HKDF_SHA512,
+ KdfId.HKDF_SHA512, AeadId.ChaCha20Poly1305, ChunkLength.KiB_1, RECEIVER_KEY_IDENTIFIER),
+ null, null),
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(Mode.psk(new byte[1]), KemId.DHKEM_P521_HKDF_SHA512,
+ KdfId.HKDF_SHA512, AeadId.ChaCha20Poly1305, ChunkLength.KiB_1, RECEIVER_KEY_IDENTIFIER),
+ IllegalArgumentException.class, "mode.pskId.length not " + ProtocolV1.PRE_SHARED_KEY_ID_LENGTH),
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(null, KemId.DHKEM_X25519_HKDF_SHA256, KdfId.HKDF_SHA256,
+ AeadId.AES_128_GCM, ChunkLength.KiB_1, RECEIVER_KEY_IDENTIFIER),
+ NullPointerException.class, "mode"),
+ Arguments.of((Executable) () -> new ProtocolV1(Mode.base(), null, KdfId.HKDF_SHA256, AeadId.AES_128_GCM,
+ ChunkLength.KiB_1, RECEIVER_KEY_IDENTIFIER), NullPointerException.class, "kemId"),
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256, null,
+ AeadId.AES_128_GCM, ChunkLength.KiB_1, RECEIVER_KEY_IDENTIFIER),
+ NullPointerException.class, "kdfId"),
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256,
+ KdfId.HKDF_SHA256, null, ChunkLength.KiB_1, RECEIVER_KEY_IDENTIFIER),
+ NullPointerException.class, "aeadId"),
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256,
+ KdfId.HKDF_SHA256, AeadId.AES_128_GCM, null, RECEIVER_KEY_IDENTIFIER),
+ NullPointerException.class, "chunkLength"),
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256,
+ KdfId.HKDF_SHA256, AeadId.AES_128_GCM, ChunkLength.KiB_1, null),
+ NullPointerException.class, "receiverKeyId"),
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256,
+ KdfId.HKDF_SHA256, AeadId.AES_128_GCM, ChunkLength.KiB_1, new byte[0]),
+ IllegalArgumentException.class,
+ "receiverKeyId.length not " + ProtocolV1.RECEIVER_KEY_ID_LENGTH),
+ Arguments.of(
+ (Executable) () -> new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256,
+ KdfId.HKDF_SHA256, AeadId.AES_128_GCM, ChunkLength.KiB_1,
+ new byte[ProtocolV1.RECEIVER_KEY_ID_LENGTH + 1]),
+ IllegalArgumentException.class,
+ "receiverKeyId.length not " + ProtocolV1.RECEIVER_KEY_ID_LENGTH));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestConstructorExceptions")
+ void testConstructorExceptions(Executable executable, Class extends Exception> expectedExcpetion,
+ String expectedMessage) throws Exception
+ {
+ if (expectedExcpetion == null)
+ assertDoesNotThrow(executable);
+ else
+ {
+ Exception e = assertThrowsExactly(expectedExcpetion, executable);
+ assertEquals(expectedMessage, e.getMessage());
+ }
+ }
+
+ private static Stream forTestWriteReadHeader()
+ {
+ return Stream.of(Arguments.of(V1_BASE, V1_BASE_BYTE_ARRAY, ChunkLength.KiB_1),
+ Arguments.of(V1_PSK, V1_PSK_BYTE_ARRAY, ChunkLength.MiB_1));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestWriteReadHeader")
+ void testWriteReadHeaderStream(ProtocolV1 protocol, byte[] expected, ChunkLength chunkLength) throws Exception
+ {
+ assertEquals(chunkLength, protocol.getChunkLength());
+
+ byte[] header = protocol.getCanonicalHeader();
+ assertArrayEquals(header, protocol.getCanonicalHeader());
+
+ logger.debug("Actual: {}", HexFormat.of().formatHex(header));
+ logger.debug("Expected: {}", HexFormat.of().formatHex(expected));
+
+ assertArrayEquals(expected, header);
+
+ ProtocolV1 readProtocol = ProtocolV1.from(new ByteArrayInputStream(expected));
+
+ assertEquals(protocol.getAeadId(), readProtocol.getAeadId());
+ assertEquals(protocol.getChunkLength(), readProtocol.getChunkLength());
+ assertEquals(protocol.getKdfId(), readProtocol.getKdfId());
+ assertEquals(protocol.getKemId(), readProtocol.getKemId());
+ assertEquals(protocol.getMode(), readProtocol.getMode());
+ assertArrayEquals(protocol.getReceiverKeyId(), readProtocol.getReceiverKeyId());
+ }
+
+ private static Stream forTestReadInvalidHeadersFromStream()
+ {
+ return Stream.concat(
+ Stream.of(
+ Arguments.of(ByteEncoding.concat(Mode.base().getValueAsI2osp1Byte(),
+ KemId.DHKEM_X25519_HKDF_SHA256.getIdAsI2osp2Bytes(),
+ KdfId.HKDF_SHA256.getIdAsI2osp2Bytes(), AeadId.AES_128_GCM.getIdAsI2osp2Bytes(),
+ ChunkLength.KiB_1.getExponentAsI2osp1Byte()), "Truncated stream"),
+ Arguments.of(
+ ByteEncoding.concat(new byte[] { (byte) 0xFF },
+ KemId.DHKEM_X25519_HKDF_SHA256.getIdAsI2osp2Bytes(),
+ KdfId.HKDF_SHA256.getIdAsI2osp2Bytes(), AeadId.AES_128_GCM.getIdAsI2osp2Bytes(),
+ ChunkLength.KiB_1.getExponentAsI2osp1Byte(), RECEIVER_KEY_IDENTIFIER),
+ "Mode not supported"),
+ Arguments.of(
+ ByteEncoding.concat(Mode.base().getValueAsI2osp1Byte(),
+ KemId.DHKEM_X25519_HKDF_SHA256.getIdAsI2osp2Bytes(),
+ KdfId.HKDF_SHA256.getIdAsI2osp2Bytes(), AeadId.AES_128_GCM.getIdAsI2osp2Bytes(),
+ new byte[] { (byte) 0xFF }, RECEIVER_KEY_IDENTIFIER),
+ "Chunk length exponent not supported"),
+ Arguments.of(
+ ByteEncoding.concat(Mode.base().getValueAsI2osp1Byte(),
+ KemId.DHKEM_X25519_HKDF_SHA256.getIdAsI2osp2Bytes(),
+ KdfId.HKDF_SHA256.getIdAsI2osp2Bytes(), AeadId.AES_128_GCM.getIdAsI2osp2Bytes(),
+ new byte[] { (byte) 0x10 }, RECEIVER_KEY_IDENTIFIER),
+ "Chunk length exponent not supported"),
+ Arguments.of(
+ ByteEncoding.concat(Mode.base().getValueAsI2osp1Byte(),
+ new byte[] { (byte) 0xFF, (byte) 0x00 }, KdfId.HKDF_SHA256.getIdAsI2osp2Bytes(),
+ AeadId.AES_128_GCM.getIdAsI2osp2Bytes(),
+ ChunkLength.KiB_1.getExponentAsI2osp1Byte(), RECEIVER_KEY_IDENTIFIER),
+ "KemId not supported"),
+ Arguments.of(ByteEncoding.concat(Mode.base().getValueAsI2osp1Byte(),
+ KemId.DHKEM_X25519_HKDF_SHA256.getIdAsI2osp2Bytes(),
+ new byte[] { (byte) 0xFF, (byte) 0xFF }, AeadId.AES_128_GCM.getIdAsI2osp2Bytes(),
+ ChunkLength.KiB_1.getExponentAsI2osp1Byte(), RECEIVER_KEY_IDENTIFIER),
+ "KdfId not supported"),
+ Arguments.of(
+ ByteEncoding.concat(Mode.base().getValueAsI2osp1Byte(),
+ KemId.DHKEM_X25519_HKDF_SHA256.getIdAsI2osp2Bytes(),
+ KdfId.HKDF_SHA256.getIdAsI2osp2Bytes(), new byte[] { (byte) 0xFF, (byte) 0xFF },
+ ChunkLength.KiB_1.getExponentAsI2osp1Byte(), RECEIVER_KEY_IDENTIFIER),
+ "AeadId not supported")),
+ Stream.concat(IntStream.range(1, V1_BASE_BYTE_ARRAY.length).mapToObj(trunc ->
+ {
+ byte[] truncated = new byte[V1_BASE_BYTE_ARRAY.length - trunc];
+ ByteBuffer.wrap(V1_PSK_BYTE_ARRAY).get(truncated);
+ return Arguments.argumentSet("Mode Base, truncated by " + trunc, truncated, "Truncated stream");
+ }), IntStream.range(1, V1_PSK_BYTE_ARRAY.length).mapToObj(trunc ->
+ {
+ byte[] truncated = new byte[V1_PSK_BYTE_ARRAY.length - trunc];
+ ByteBuffer.wrap(V1_PSK_BYTE_ARRAY).get(truncated);
+ return Arguments.argumentSet("Mode PSK, truncated by " + trunc, truncated, "Truncated stream");
+ })));
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestReadInvalidHeadersFromStream")
+ void testReadInvalidHeadersFromStream(byte[] invalid, String exceptionMessage) throws Exception
+ {
+ Exception exception = assertThrowsExactly(IOException.class,
+ () -> ProtocolV1.from(new ByteArrayInputStream(invalid)));
+ assertEquals(exceptionMessage, exception.getMessage());
+ }
+
+ private static Stream forTestGetChunkSize()
+ {
+ return EnumSet.allOf(ChunkLength.class).stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestGetChunkSize")
+ void testGetChunkSize(ChunkLength chunkLength)
+ {
+ ProtocolV1 protocol = new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256, KdfId.HKDF_SHA256,
+ AeadId.AES_128_GCM, chunkLength, RECEIVER_KEY_IDENTIFIER);
+
+ assertEquals(chunkLength, protocol.getChunkLength());
+ }
+
+ @Test
+ void testGetKdfInfo() throws Exception
+ {
+ ProtocolV1 protocol = new ProtocolV1(Mode.base(), KemId.DHKEM_X25519_HKDF_SHA256, KdfId.HKDF_SHA256,
+ AeadId.AES_128_GCM, ChunkLength.KiB_1, RECEIVER_KEY_IDENTIFIER);
+
+ assertNotNull(protocol.getKdfInfo());
+ assertArrayEquals(new byte[] { 'H', 'P', 'K', 'E', 'F', (byte) 0x01, (byte) 0x00 }, protocol.getKdfInfo());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/RsaKemWrapperTest.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/RsaKemWrapperTest.java
new file mode 100644
index 0000000..3b359be
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/RsaKemWrapperTest.java
@@ -0,0 +1,202 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.stream.Stream;
+
+import javax.crypto.DecapsulateException;
+import javax.crypto.KEM.Encapsulated;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.bouncycastle.crypto.DerivationFunction;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.bouncycastle.crypto.digests.SHA512Digest;
+import org.bouncycastle.crypto.generators.KDF2BytesGenerator;
+import org.bouncycastle.crypto.kems.RSAKEMExtractor;
+import org.bouncycastle.crypto.params.RSAKeyParameters;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory;
+
+public class RsaKemWrapperTest
+{
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ private static Stream forTestConstructor()
+ {
+ return EnumSet.complementOf(EnumSet.of(KemId.RSAKEM_1024_KDF2_SHA256, KemId.RSAKEM_2048_KDF2_SHA256,
+ KemId.RSAKEM_3072_KDF2_SHA512, KemId.RSAKEM_4096_KDF2_SHA512)).stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("forTestConstructor")
+ void testConstructor(KemId kemId) throws Exception
+ {
+ IllegalArgumentException e = assertThrowsExactly(IllegalArgumentException.class,
+ () -> new RsaKemWrapper(kemId));
+ assertEquals("KemId " + kemId.name() + " not supported", e.getMessage());
+ }
+
+ private static Stream kemVariants()
+ {
+ KDF2BytesGenerator kdf2Sha256 = new KDF2BytesGenerator(new SHA256Digest());
+ KDF2BytesGenerator kdf2Sha512 = new KDF2BytesGenerator(new SHA512Digest());
+
+ return Stream.of(
+ Arguments.of(KemId.RSAKEM_1024_KDF2_SHA256,
+ KeyPairGeneratorFactory.rsa1024().initialize().generateKeyPair(), kdf2Sha256),
+ Arguments.of(KemId.RSAKEM_2048_KDF2_SHA256,
+ KeyPairGeneratorFactory.rsa2048().initialize().generateKeyPair(), kdf2Sha256),
+ Arguments.of(KemId.RSAKEM_3072_KDF2_SHA512,
+ KeyPairGeneratorFactory.rsa3072().initialize().generateKeyPair(), kdf2Sha512),
+ Arguments.of(KemId.RSAKEM_4096_KDF2_SHA512,
+ KeyPairGeneratorFactory.rsa4096().initialize().generateKeyPair(), kdf2Sha512));
+ }
+
+ @ParameterizedTest
+ @MethodSource("kemVariants")
+ void testAgainstBouncyCastleImplementation(KemId kemId, KeyPair keyPair, DerivationFunction kdf) throws Exception
+ {
+ RsaKemWrapper w = new RsaKemWrapper(kemId);
+ Encapsulated encapsulated = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+
+ assertNotNull(encapsulated);
+
+ SecretKey sKey = encapsulated.key();
+ assertNotNull(sKey);
+ assertEquals("Generic", sKey.getAlgorithm());
+ assertNotNull(sKey.getEncoded());
+
+ byte[] encapsulation = encapsulated.encapsulation();
+ assertNotNull(encapsulation);
+ assertEquals(kemId.getEncapsulationLength(), encapsulation.length);
+
+ SecretKey rKey = w.getSharedSecret(keyPair.getPrivate(), encapsulation);
+ assertNotNull(rKey);
+ assertEquals("Generic", rKey.getAlgorithm());
+ assertNotNull(rKey.getEncoded());
+
+ assertEquals(sKey, rKey);
+
+ SecretKey rKeyBC = bouncyCastledoGetSecretKey(keyPair.getPrivate(), encapsulation,
+ kemId.getSharedSecretLength(), kdf);
+ assertNotNull(rKeyBC);
+ assertEquals("Generic", rKeyBC.getAlgorithm());
+ assertNotNull(rKeyBC.getEncoded());
+
+ assertEquals(sKey, rKeyBC);
+ }
+
+ private SecretKey bouncyCastledoGetSecretKey(PrivateKey privateKey, byte[] encapsulation, int sharedSecretLength,
+ DerivationFunction kdf) throws NoSuchAlgorithmException, InvalidKeyException, DecapsulateException
+ {
+ RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) privateKey;
+
+ RSAKeyParameters rsaKeyParameters = new RSAKeyParameters(true, rsaPrivateKey.getModulus(),
+ rsaPrivateKey.getPrivateExponent());
+
+ RSAKEMExtractor decapsulator = new RSAKEMExtractor(rsaKeyParameters, sharedSecretLength, kdf);
+
+ return new SecretKeySpec(decapsulator.extractSecret(encapsulation), "Generic");
+ }
+
+ @ParameterizedTest
+ @MethodSource("kemVariants")
+ void testTruncatedEnc(KemId kemId, KeyPair keyPair) throws Exception
+ {
+ RsaKemWrapper w = new RsaKemWrapper(kemId);
+ Encapsulated encapsulated = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+
+ assertNotNull(encapsulated);
+
+ SecretKey sKey = encapsulated.key();
+ assertNotNull(sKey);
+ assertEquals("Generic", sKey.getAlgorithm());
+ assertNotNull(sKey.getEncoded());
+
+ byte[] encapsulation = encapsulated.encapsulation();
+ assertNotNull(encapsulation);
+ assertEquals(kemId.getEncapsulationLength(), encapsulation.length);
+
+ for (int i = 0; i <= encapsulation.length; i++)
+ {
+ try
+ {
+ w.getSharedSecret(keyPair.getPrivate(), Arrays.copyOfRange(encapsulation, 0, encapsulation.length - i));
+ assertEquals(0, i); // only not truncated stream ok
+ }
+ catch (IllegalStateException e)
+ {
+ assertEquals("encapsulation.length not " + kemId.getEncapsulationLength(), e.getMessage());
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("kemVariants")
+ void testModifiedEnc(KemId kemId, KeyPair keyPair) throws Exception
+ {
+ RsaKemWrapper w = new RsaKemWrapper(kemId);
+ Encapsulated encapsulated = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+
+ assertNotNull(encapsulated);
+
+ SecretKey sKey = encapsulated.key();
+ assertNotNull(sKey);
+ assertEquals("Generic", sKey.getAlgorithm());
+ assertNotNull(sKey.getEncoded());
+
+ byte[] encapsulation = encapsulated.encapsulation();
+ assertNotNull(encapsulation);
+ assertEquals(kemId.getEncapsulationLength(), encapsulation.length);
+
+ for (int i = 0; i < encapsulation.length; i++)
+ {
+ encapsulation[i] ^= 0x01;
+ SecretKey sharedSecret = w.getSharedSecret(keyPair.getPrivate(), encapsulation);
+
+ assertNotSame(sKey, sharedSecret);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("kemVariants")
+ void testTwoCallsResultInDifferentEncapsulationsAndKeys(KemId kemId, KeyPair keyPair) throws Exception
+ {
+ RsaKemWrapper w = new RsaKemWrapper(kemId);
+
+ Encapsulated e1 = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+ assertNotNull(e1);
+ assertNotNull(e1.encapsulation());
+ assertNotNull(e1.key());
+
+ Encapsulated e2 = w.getEncapsulated(keyPair.getPublic(), SECURE_RANDOM);
+ assertNotNull(e2);
+ assertNotNull(e2.encapsulation());
+ assertNotNull(e2.key());
+
+ assertFalse(Arrays.equals(e1.encapsulation(), e2.encapsulation()));
+ assertFalse(Arrays.equals(e1.key().getEncoded(), e2.key().getEncoded()));
+
+ SecretKey sharedSecret1 = w.getSharedSecret(keyPair.getPrivate(), e1.encapsulation());
+ assertArrayEquals(e1.key().getEncoded(), sharedSecret1.getEncoded());
+ SecretKey sharedSecret2 = w.getSharedSecret(keyPair.getPrivate(), e2.encapsulation());
+ assertArrayEquals(e2.key().getEncoded(), sharedSecret2.getEncoded());
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/Sha256.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/Sha256.java
new file mode 100644
index 0000000..aefff08
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/Sha256.java
@@ -0,0 +1,24 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public final class Sha256
+{
+ private Sha256()
+ {
+ }
+
+ public static byte[] digest(byte[] bytes)
+ {
+ try
+ {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return digest.digest(bytes);
+ }
+ catch (NoSuchAlgorithmException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ZeroInputStream.java b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ZeroInputStream.java
new file mode 100644
index 0000000..3155d7c
--- /dev/null
+++ b/src/test/java/de/hsheilbronn/mi/utils/crypto/hpke/ZeroInputStream.java
@@ -0,0 +1,61 @@
+package de.hsheilbronn.mi.utils.crypto.hpke;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public final class ZeroInputStream extends InputStream
+{
+ private final long size;
+ private long position = 0;
+
+ public ZeroInputStream(long size)
+ {
+ this.size = size;
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ if (position >= size)
+ return -1;
+
+ position++;
+ return 0;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException
+ {
+ if (position >= size)
+ return -1;
+
+ int bytesToRead = (int) Math.min(len, size - position);
+
+ for (int i = 0; i < bytesToRead; i++)
+ b[off + i] = 0;
+
+ position += bytesToRead;
+ return bytesToRead;
+ }
+
+ @Override
+ public long skip(long n) throws IOException
+ {
+ long skipped = Math.min(n, size - position);
+ position += skipped;
+ return skipped;
+ }
+
+ @Override
+ public int available() throws IOException
+ {
+ long remaining = size - position;
+ return remaining > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) remaining;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ // nothing to do
+ }
+}