diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java
index 5faa5dc..b2cd805 100644
--- a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java
+++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java
@@ -76,7 +76,7 @@ import zeroecho.core.context.AgreementContext;
*
* @since 1.0
*/
-public final class GenericJcaAgreementContext implements AgreementContext {
+public class GenericJcaAgreementContext implements AgreementContext {
private final CryptoAlgorithm algorithm;
private final PrivateKey privateKey;
private final String jcaName; // e.g., "ECDH" or "XDH" (or "X25519"/"X448")
diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java
new file mode 100644
index 0000000..8625e7b
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java
@@ -0,0 +1,235 @@
+/*******************************************************************************
+ * Copyright (C) 2025, Leo Galambos All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. All advertising materials mentioning features or use of this software must
+ * display the following acknowledgement: This product includes software
+ * developed by the Egothor project.
+ *
+ * 4. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ******************************************************************************/
+package zeroecho.core.alg.common.agreement;
+
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Objects;
+
+import zeroecho.core.CryptoAlgorithm;
+import zeroecho.core.context.MessageAgreementContext;
+
+/**
+ * Message-oriented JCA key agreement context where the handshake message is a
+ * public key encoding.
+ *
+ *
+ * This context provides a {@link MessageAgreementContext} view over a classical
+ * JCA Diffie–Hellman style agreement (e.g., ECDH, XDH). The protocol
+ * "to-be-sent" data for such agreements is the party's public key. This class
+ * therefore maps:
+ *
+ *
+ * - {@link #getPeerMessage()} to the local public key encoding (X.509
+ * SubjectPublicKeyInfo),
+ * - {@link #setPeerMessage(byte[])} to importing the peer public key and
+ * binding it as the peer key.
+ *
+ *
+ * Encoding
+ *
+ * The message format is the standard X.509 SubjectPublicKeyInfo encoding
+ * returned by {@link java.security.PublicKey#getEncoded()}. This format is
+ * stable, widely interoperable and avoids ad-hoc or algorithm-specific wire
+ * formats.
+ *
+ *
+ * Algorithm and provider selection
+ *
+ * The underlying key agreement algorithm name (for
+ * {@link javax.crypto.KeyAgreement}) and the key factory algorithm name (for
+ * {@link KeyFactory}) are supplied explicitly by the algorithm registration
+ * code. This keeps algorithm-specific knowledge in {@code *Algorithm} classes
+ * rather than embedding naming heuristics into shared code.
+ *
+ *
+ * Lifecycle
+ *
+ * - Create the context with the local key pair wrapper.
+ * - Send {@link #getPeerMessage()} to the remote party.
+ * - Receive the remote party's message and call
+ * {@link #setPeerMessage(byte[])}.
+ * - Derive the raw shared secret with {@link #deriveSecret()}.
+ *
+ *
+ * Security considerations
+ *
+ * - The message returned by {@link #getPeerMessage()} contains only public
+ * key material.
+ * - The output of {@link #deriveSecret()} is a raw shared secret and must be
+ * processed with a KDF by higher protocol layers before use as symmetric keying
+ * material.
+ * - This class does not log, persist, or otherwise expose private key
+ * material.
+ *
+ *
+ * Thread safety
+ *
+ * Instances are not thread-safe and are intended for single-use,
+ * single-threaded protocol executions.
+ *
+ *
+ * @since 1.0
+ */
+public final class GenericJcaMessageAgreementContext extends GenericJcaAgreementContext
+ implements MessageAgreementContext {
+
+ private final PublicKey localPublic;
+ private final String keyFactoryAlg;
+ private final String keyFactoryProvider;
+
+ /**
+ * Creates a message-oriented agreement context over a JCA key agreement.
+ *
+ *
+ * The wrapped {@link KeyPairKey} supplies both local private and public key
+ * components: the private key is used for the underlying agreement computation,
+ * while the public key is exported as the handshake message via
+ * {@link #getPeerMessage()}.
+ *
+ *
+ * @param alg algorithm descriptor that owns this context
+ * @param keyPairKey local key pair wrapper; must contain both a private
+ * and a public key
+ * @param jcaAgreementName JCA {@link javax.crypto.KeyAgreement} algorithm
+ * name (e.g., {@code "ECDH"}, {@code "X25519"})
+ * @param agreementProvider optional JCA provider name for
+ * {@link javax.crypto.KeyAgreement}, or {@code null}
+ * for default provider selection
+ * @param keyFactoryAlg JCA {@link KeyFactory} algorithm name used to
+ * import peer public keys (e.g., {@code "EC"},
+ * {@code "XDH"})
+ * @param keyFactoryProvider optional JCA provider name for {@link KeyFactory},
+ * or {@code null} for default provider selection
+ * @throws NullPointerException if any required argument is {@code null}
+ * @since 1.0
+ */
+ public GenericJcaMessageAgreementContext(CryptoAlgorithm alg, KeyPairKey keyPairKey, String jcaAgreementName,
+ String agreementProvider, String keyFactoryAlg, String keyFactoryProvider) {
+ super(Objects.requireNonNull(alg, "alg"), Objects.requireNonNull(keyPairKey, "keyPairKey").privateKey(),
+ Objects.requireNonNull(jcaAgreementName, "jcaAgreementName"), agreementProvider);
+ this.localPublic = Objects.requireNonNull(keyPairKey.publicKey(), "keyPairKey.public");
+ this.keyFactoryAlg = Objects.requireNonNull(keyFactoryAlg, "keyFactoryAlg");
+ this.keyFactoryProvider = keyFactoryProvider;
+ }
+
+ /**
+ * Returns the local party's handshake message.
+ *
+ *
+ * For DH/XDH-style agreements, the handshake message is the local public key
+ * encoding. The returned array is a defensive copy and may be safely
+ * transmitted over untrusted channels.
+ *
+ *
+ *
+ * The encoding is the standard X.509 SubjectPublicKeyInfo bytes returned by
+ * {@link PublicKey#getEncoded()}.
+ *
+ *
+ * @return a defensive copy of the local public key encoding (never
+ * {@code null})
+ * @throws IllegalStateException if the local public key does not provide an
+ * encoding
+ * @since 1.0
+ */
+ @Override
+ public byte[] getPeerMessage() {
+ byte[] encoded = localPublic.getEncoded();
+ if (encoded == null) {
+ throw new IllegalStateException("Local public key does not provide an encoding");
+ }
+ return encoded.clone();
+ }
+
+ /**
+ * Supplies the peer party's handshake message.
+ *
+ *
+ * The provided message is interpreted as an X.509 SubjectPublicKeyInfo encoding
+ * of the peer public key. The key is imported using {@link KeyFactory} and then
+ * assigned as the peer key for the underlying agreement computation.
+ *
+ *
+ *
+ * Passing {@code null} resets the peer binding and makes
+ * {@link #deriveSecret()} unusable until a new peer message is provided.
+ *
+ *
+ * @param message peer public key encoding (SPKI), or {@code null} to reset the
+ * peer state
+ * @throws IllegalArgumentException if the message cannot be imported as a
+ * public key using the configured
+ * {@link KeyFactory} algorithm/provider
+ * @since 1.0
+ */
+ @Override
+ public void setPeerMessage(byte[] message) {
+ if (message == null) {
+ setPeerPublic(null);
+ return;
+ }
+
+ PublicKey peerPublic = importPeerPublic(message);
+ setPeerPublic(peerPublic);
+ }
+
+ /**
+ * Imports a peer public key from an X.509 SubjectPublicKeyInfo encoding.
+ *
+ *
+ * This method performs no caching and always imports the key anew. The caller
+ * is responsible for ensuring that the protocol-layer validation requirements
+ * are met (e.g., checking that the received public key belongs to an expected
+ * identity, or that the agreement mode requires ephemeral vs. static keys).
+ *
+ *
+ * @param spkiEncoded peer public key encoding (SPKI); must not be {@code null}
+ * @return imported peer {@link PublicKey}
+ * @throws IllegalArgumentException if the key cannot be imported
+ */
+ private PublicKey importPeerPublic(byte[] spkiEncoded) {
+ try {
+ KeyFactory keyFactory = (keyFactoryProvider == null) ? KeyFactory.getInstance(keyFactoryAlg)
+ : KeyFactory.getInstance(keyFactoryAlg, keyFactoryProvider);
+
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(spkiEncoded);
+ return keyFactory.generatePublic(spec);
+ } catch (GeneralSecurityException e) {
+ throw new IllegalArgumentException("Failed to import peer public key using KeyFactory " + keyFactoryAlg, e);
+ }
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/KeyPairKey.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/KeyPairKey.java
new file mode 100644
index 0000000..e9e87d3
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/KeyPairKey.java
@@ -0,0 +1,147 @@
+/*******************************************************************************
+ * Copyright (C) 2025, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. All advertising materials mentioning features or use of this software must
+ * display the following acknowledgement:
+ * This product includes software developed by the Egothor project.
+ *
+ * 4. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ******************************************************************************/
+package zeroecho.core.alg.common.agreement;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Objects;
+
+/**
+ * A {@link Key} wrapper that carries a {@link KeyPair} through APIs that are
+ * constrained to {@link Key} types.
+ *
+ *
+ * This type exists to support capabilities that require both private and public
+ * components (e.g., message-oriented key agreement contexts), while preserving
+ * backward-compatible capabilities that accept only a {@link PrivateKey}.
+ *
+ *
+ *
+ * The wrapper does not expose an encoding via {@link #getEncoded()} because
+ * serializing private key material implicitly is dangerous and not required for
+ * capability dispatch.
+ *
+ *
+ * @since 1.0
+ */
+public final class KeyPairKey implements Key, Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private final KeyPair keyPair;
+
+ /**
+ * Creates a wrapper around a {@link KeyPair}.
+ *
+ * @param keyPair key pair to wrap (must not be {@code null})
+ * @throws NullPointerException if {@code keyPair} is {@code null}
+ * @since 1.0
+ */
+ public KeyPairKey(KeyPair keyPair) {
+ this.keyPair = Objects.requireNonNull(keyPair, "keyPair");
+ Objects.requireNonNull(keyPair.getPrivate(), "keyPair.private");
+ Objects.requireNonNull(keyPair.getPublic(), "keyPair.public");
+ }
+
+ /**
+ * Returns the wrapped {@link KeyPair}.
+ *
+ * @return key pair
+ * @since 1.0
+ */
+ public KeyPair keyPair() {
+ return keyPair;
+ }
+
+ /**
+ * Returns the wrapped private key.
+ *
+ * @return private key
+ * @since 1.0
+ */
+ public PrivateKey privateKey() {
+ return keyPair.getPrivate();
+ }
+
+ /**
+ * Returns the wrapped public key.
+ *
+ * @return public key
+ * @since 1.0
+ */
+ public PublicKey publicKey() {
+ return keyPair.getPublic();
+ }
+
+ /**
+ * Returns the algorithm name of the wrapped private key.
+ *
+ * @return algorithm name
+ * @since 1.0
+ */
+ @Override
+ public String getAlgorithm() {
+ return privateKey().getAlgorithm();
+ }
+
+ /**
+ * Returns {@code null}. This wrapper intentionally does not define a standard
+ * encoding format.
+ *
+ * @return {@code null}
+ * @since 1.0
+ */
+ @Override
+ public String getFormat() {
+ return null;
+ }
+
+ /**
+ * Returns {@code null}. This wrapper intentionally does not expose a combined
+ * encoding.
+ *
+ * @return {@code null}
+ * @since 1.0
+ */
+ @Override
+ public byte[] getEncoded() {
+ return null; // NOPMD
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java
index 6df5525..8fce875 100644
--- a/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java
@@ -46,7 +46,10 @@ import zeroecho.core.AlgorithmFamily;
import zeroecho.core.KeyUsage;
import zeroecho.core.alg.AbstractCryptoAlgorithm;
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
+import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
+import zeroecho.core.alg.common.agreement.KeyPairKey;
import zeroecho.core.context.AgreementContext;
+import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spi.AsymmetricKeyBuilder;
/**
@@ -133,6 +136,10 @@ public final class DhAlgorithm extends AbstractCryptoAlgorithm {
DhSpec.class,
(PrivateKey k, DhSpec s) -> new GenericJcaAgreementContext(this, k, "DiffieHellman", null),
DhSpec::ffdhe2048);
+ capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, KeyPairKey.class,
+ DhSpec.class, (KeyPairKey k, DhSpec s) -> new GenericJcaMessageAgreementContext(this, k,
+ "DiffieHellman", null, "DH", null),
+ DhSpec::ffdhe2048);
registerAsymmetricKeyBuilder(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048);
registerAsymmetricKeyBuilder(DhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java
index 9e0457d..afcfa19 100644
--- a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java
@@ -40,12 +40,15 @@ import zeroecho.core.AlgorithmFamily;
import zeroecho.core.KeyUsage;
import zeroecho.core.alg.AbstractCryptoAlgorithm;
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
+import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
+import zeroecho.core.alg.common.agreement.KeyPairKey;
import zeroecho.core.alg.ecdsa.EcdsaCurveSpec;
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder;
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec;
import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder;
import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec;
import zeroecho.core.context.AgreementContext;
+import zeroecho.core.context.MessageAgreementContext;
/**
* Elliptic Curve Diffie-Hellman (ECDH) Algorithm
@@ -152,6 +155,10 @@ public final class EcdhAlgorithm extends AbstractCryptoAlgorithm {
EcdsaCurveSpec.class,
(PrivateKey k, EcdsaCurveSpec s) -> new GenericJcaAgreementContext(this, k, "ECDH", null),
() -> EcdsaCurveSpec.P256); // XXX spec is not used at all ?!?!
+ capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, KeyPairKey.class,
+ EcdsaCurveSpec.class, (KeyPairKey k, EcdsaCurveSpec s) -> new GenericJcaMessageAgreementContext(this, k,
+ "ECDH", null, "EC", null),
+ () -> EcdsaCurveSpec.P256);
// Reuse EC builders/importers
registerAsymmetricKeyBuilder(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256);
diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java
index 346f372..bd542b5 100644
--- a/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java
@@ -46,7 +46,10 @@ import zeroecho.core.AlgorithmFamily;
import zeroecho.core.KeyUsage;
import zeroecho.core.alg.AbstractCryptoAlgorithm;
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
+import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
+import zeroecho.core.alg.common.agreement.KeyPairKey;
import zeroecho.core.context.AgreementContext;
+import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spi.AsymmetricKeyBuilder;
/**
@@ -145,6 +148,11 @@ public final class XdhAlgorithm extends AbstractCryptoAlgorithm {
XdhSpec.class,
(PrivateKey k, XdhSpec s) -> new GenericJcaAgreementContext(this, k, s.keyAgreementName(), null),
() -> XdhSpec.X25519);
+ // New capability: MessageAgreementContext over KeyPair
+ capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, KeyPairKey.class,
+ XdhSpec.class, (KeyPairKey k, XdhSpec s) -> new GenericJcaMessageAgreementContext(this, k,
+ s.keyAgreementName(), null, "XDH", null),
+ () -> XdhSpec.X25519);
registerAsymmetricKeyBuilder(XdhSpec.class, new XdhKeyGenBuilder(), () -> XdhSpec.X25519);
registerAsymmetricKeyBuilder(XdhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
diff --git a/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java b/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java
index 12f1a7a..32d3b34 100644
--- a/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java
+++ b/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java
@@ -36,7 +36,6 @@ package zeroecho.core.alg.common.agreement;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
@@ -88,20 +87,26 @@ public class AgreementAlgorithmsRoundTripTest {
System.out.println(method + "...ok");
}
+ private static void printCapabilityHeader(String algId, String branch, Capability cap) {
+ System.out.println("...algId=" + algId + ", branch=" + branch + ", ctxType=" + cap.contextType().getSimpleName()
+ + ", keyType=" + cap.keyType().getSimpleName() + ", specType=" + cap.specType().getSimpleName());
+ }
+
private static String hex(byte[] b) {
if (b == null) {
return "null";
}
StringBuilder sb = new StringBuilder(b.length * 2);
- for (byte v : b) {
+ for (int i = 0; i < b.length; i++) {
if (sb.length() == 80) {
sb.append("...");
break;
}
- if ((v & 0xFF) < 16) {
+ int v = b[i] & 0xFF;
+ if (v < 16) {
sb.append('0');
}
- sb.append(Integer.toHexString(v & 0xFF));
+ sb.append(Integer.toHexString(v));
}
return sb.toString();
}
@@ -118,8 +123,13 @@ public class AgreementAlgorithmsRoundTripTest {
@BeforeAll
static void setup() {
- // If needed, install/activate providers (e.g., BouncyCastlePQCProvider) here.
- BouncyCastleActivator.init();
+ // Optional: activate providers (e.g., BC/BCPQC) if present.
+ // Keep tests runnable even if BC is absent.
+ try {
+ BouncyCastleActivator.init();
+ } catch (Throwable ignore) {
+ // ignore
+ }
}
@Test
@@ -135,131 +145,254 @@ public class AgreementAlgorithmsRoundTripTest {
}
final List caps = alg.listCapabilities();
-
for (Capability cap : caps) {
if (cap.role() != KeyUsage.AGREEMENT) {
continue;
}
- System.out.printf(" ...%s - AGREEMENT capability found", id);
+ System.out.println("...algId=" + id + " - AGREEMENT capability found");
final Class> ctxType = cap.contextType();
final Class> keyType = cap.keyType();
final Class> specType = cap.specType();
- @SuppressWarnings("unused")
- final ContextSpec defVal = cap.defaultSpec().get();
- // System.out.printf(" ......type=[%s] key=[%s] spec=[%s] default=[%s]%n",
- // ctxType, keyType, specType, defVal);
+ // --------------------------------------------------------------------
+ // MessageAgreementContext branch A: PAIR_MESSAGE (DH/XDH/ECDH-style)
+ //
+ // New extension:
+ // - contextType: MessageAgreementContext
+ // - keyType: KeyPairKey (wrapper for KeyPair, because capabilities require Key)
+ // - specType: ContextSpec (algorithm-specific agreement spec)
+ //
+ // Semantics:
+ // - getPeerMessage() returns local public key encoding (SPKI)
+ // - setPeerMessage(...) imports peer public key encoding
+ // --------------------------------------------------------------------
+ if (MessageAgreementContext.class.isAssignableFrom(ctxType) && keyType == KeyPairKey.class
+ && ContextSpec.class.isAssignableFrom(specType)) {
- // AGREEMENT (KEM-style) via MessageAgreementContext adapter
- if (MessageAgreementContext.class.isAssignableFrom(ctxType)) {
+ printCapabilityHeader(alg.id(), "PAIR_MESSAGE", cap);
- KeyPair bob = generateKeyPair(alg);
- if (bob == null) {
- System.out.println(" ...bob=null");
+ ContextSpec spec = null;
+ try {
+ spec = cap.defaultSpec().get();
+ } catch (Throwable ignore) {
+ spec = tryExtractContextSpec(alg);
+ }
+ if (spec == null) {
+ System.out.println("...spec=null (skip)");
continue;
}
- System.out.println("runAgreement(" + alg.id() + ", VoidSpec)");
- System.out.println(" Bob.public " + keyInfo("key", bob.getPublic()));
- System.out.println(" Bob.private " + keyInfo("key", bob.getPrivate()));
-
- // Alice (initiator): has Bob's public key
- MessageAgreementContext aliceCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT,
- bob.getPublic(), VoidSpec.INSTANCE);
-
- // Bob (responder): has his private key
- MessageAgreementContext bobCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT,
- bob.getPrivate(), VoidSpec.INSTANCE);
-
- // Initiator produces encapsulation message (ciphertext) to send
- byte[] enc = aliceCtx.getPeerMessage();
- // Responder consumes it
- bobCtx.setPeerMessage(enc);
-
- // Both derive the same shared secret
- byte[] kA = aliceCtx.deriveSecret();
- byte[] kB = bobCtx.deriveSecret();
-
- System.out.println(" KEM.ciphertext=" + hex(enc));
- System.out.println(" Alice.secret =" + hex(kA));
- System.out.println(" Bob.secret =" + hex(kB));
-
- assertArrayEquals(kA, kB, alg.id() + ": agreement secrets mismatch");
- System.out.println("...ok");
-
- try {
- aliceCtx.close();
- } catch (Exception ignored) {
+ KeyPair alice = generateKeyPair(alg);
+ KeyPair bob = generateKeyPair(alg);
+ if (alice == null || bob == null) {
+ System.out.println("...keypair=null (skip)");
+ continue;
}
+
+ System.out.println("...pair-message agreement roundtrip: algId=" + alg.id() + ", spec="
+ + spec.getClass().getSimpleName());
+ System.out.println("...Alice.public " + keyInfo("key", alice.getPublic()));
+ System.out.println("...Alice.private " + keyInfo("key", alice.getPrivate()));
+ System.out.println("...Bob.public " + keyInfo("key", bob.getPublic()));
+ System.out.println("...Bob.private " + keyInfo("key", bob.getPrivate()));
+
+ KeyPairKey aliceKey = new KeyPairKey(alice);
+ KeyPairKey bobKey = new KeyPairKey(bob);
+
+ MessageAgreementContext aCtx = null;
+ MessageAgreementContext bCtx = null;
+
try {
- bobCtx.close();
- } catch (Exception ignored) {
+ aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, aliceKey, spec);
+ bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bobKey, spec);
+
+ byte[] aMsg = aCtx.getPeerMessage();
+ byte[] bMsg = bCtx.getPeerMessage();
+
+ aCtx.setPeerMessage(bMsg);
+ bCtx.setPeerMessage(aMsg);
+
+ byte[] zA = aCtx.deriveSecret();
+ byte[] zB = bCtx.deriveSecret();
+
+ System.out.println("...Alice.msg =" + hex(aMsg));
+ System.out.println("...Bob.msg =" + hex(bMsg));
+ System.out.println("...Alice.secret =" + hex(zA));
+ System.out.println("...Bob.secret =" + hex(zB));
+
+ assertArrayEquals(zA, zB, alg.id() + ": PAIR_MESSAGE secrets mismatch");
+ System.out.println("...PAIR_MESSAGE...ok");
+ } finally {
+ if (aCtx != null) {
+ try {
+ aCtx.close();
+ } catch (Exception ignored) {
+ // ignore
+ }
+ }
+ if (bCtx != null) {
+ try {
+ bCtx.close();
+ } catch (Exception ignored) {
+ // ignore
+ }
+ }
+ }
+
+ // For a given algorithm, one AGREEMENT capability is sufficient for the sweep.
+ break;
+ }
+
+ // --------------------------------------------------------------------
+ // MessageAgreementContext branch B: KEM_ADAPTER (KEM-style adapter)
+ //
+ // Existing behavior:
+ // - initiator has recipient PublicKey (encapsulation)
+ // - responder has PrivateKey (decapsulation)
+ // - spec is VoidSpec
+ // --------------------------------------------------------------------
+ if (MessageAgreementContext.class.isAssignableFrom(ctxType)
+ && (keyType == PublicKey.class || keyType == PrivateKey.class)) {
+
+ printCapabilityHeader(alg.id(), "KEM_ADAPTER", cap);
+
+ KeyPair bob = generateKeyPair(alg);
+ if (bob == null) {
+ System.out.println("...keypair=null (skip)");
+ continue;
+ }
+
+ System.out.println("...kem-adapter agreement roundtrip: algId=" + alg.id() + ", spec=VoidSpec");
+ System.out.println("...Bob.public " + keyInfo("key", bob.getPublic()));
+ System.out.println("...Bob.private " + keyInfo("key", bob.getPrivate()));
+
+ MessageAgreementContext aliceCtx = null;
+ MessageAgreementContext bobCtx = null;
+
+ try {
+ // Alice (initiator): has Bob's public key
+ aliceCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPublic(),
+ VoidSpec.INSTANCE);
+
+ // Bob (responder): has his private key
+ bobCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(),
+ VoidSpec.INSTANCE);
+
+ // Initiator produces encapsulation message (ciphertext) to send
+ byte[] enc = aliceCtx.getPeerMessage();
+ // Responder consumes it
+ bobCtx.setPeerMessage(enc);
+
+ // Both derive the same shared secret
+ byte[] kA = aliceCtx.deriveSecret();
+ byte[] kB = bobCtx.deriveSecret();
+
+ System.out.println("...KEM.ciphertext=" + hex(enc));
+ System.out.println("...Alice.secret =" + hex(kA));
+ System.out.println("...Bob.secret =" + hex(kB));
+
+ assertArrayEquals(kA, kB, alg.id() + ": KEM_ADAPTER secrets mismatch");
+ System.out.println("...KEM_ADAPTER...ok");
+ } finally {
+ if (aliceCtx != null) {
+ try {
+ aliceCtx.close();
+ } catch (Exception ignored) {
+ // ignore
+ }
+ }
+ if (bobCtx != null) {
+ try {
+ bobCtx.close();
+ } catch (Exception ignored) {
+ // ignore
+ }
+ }
}
break;
}
- // -------- Classic DH/XDH: AgreementContext + real ContextSpec --------
- else if (AgreementContext.class.isAssignableFrom(ctxType)
- && ContextSpec.class.isAssignableFrom(specType) && keyType == PrivateKey.class) {
+ // --------------------------------------------------------------------
+ // AgreementContext branch C: CLASSIC_AGREEMENT (PrivateKey + ContextSpec)
+ // --------------------------------------------------------------------
+ if (AgreementContext.class.isAssignableFrom(ctxType) && keyType == PrivateKey.class
+ && ContextSpec.class.isAssignableFrom(specType)) {
- KeyPair alice;
- KeyPair bob;
+ printCapabilityHeader(alg.id(), "CLASSIC_AGREEMENT", cap);
- alice = generateKeyPair(alg);
- bob = generateKeyPair(alg);
-
- if (alice == null || bob == null) {
- continue;
- }
-
- // Prefer the capability's default ContextSpec if provided
ContextSpec spec = null;
try {
- ContextSpec def = cap.defaultSpec().get();
- spec = def;
+ spec = cap.defaultSpec().get();
} catch (Throwable ignore) {
spec = tryExtractContextSpec(alg);
}
if (spec == null) {
+ System.out.println("...spec=null (skip)");
continue;
}
- System.out.println("runAgreement(" + alg.id() + ", " + spec.getClass().getSimpleName() + ")");
- System.out.println(" Alice.public " + keyInfo("key", alice.getPublic()));
- System.out.println(" Alice.private " + keyInfo("key", alice.getPrivate()));
- System.out.println(" Bob.public " + keyInfo("key", bob.getPublic()));
- System.out.println(" Bob.private " + keyInfo("key", bob.getPrivate()));
+ KeyPair alice = generateKeyPair(alg);
+ KeyPair bob = generateKeyPair(alg);
- // assertDhCompatible(alice.getPrivate(), bob.getPublic());
- // assertDhCompatible(bob.getPrivate(), alice.getPublic());
+ if (alice == null || bob == null) {
+ System.out.println("...keypair=null (skip)");
+ continue;
+ }
- AgreementContext aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(),
- spec);
- AgreementContext bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(),
- spec);
- aCtx.setPeerPublic(bob.getPublic());
- bCtx.setPeerPublic(alice.getPublic());
+ System.out.println("...classic agreement roundtrip: algId=" + alg.id() + ", spec="
+ + spec.getClass().getSimpleName());
+ System.out.println("...Alice.public " + keyInfo("key", alice.getPublic()));
+ System.out.println("...Alice.private " + keyInfo("key", alice.getPrivate()));
+ System.out.println("...Bob.public " + keyInfo("key", bob.getPublic()));
+ System.out.println("...Bob.private " + keyInfo("key", bob.getPrivate()));
- byte[] zA = aCtx.deriveSecret();
- byte[] zB = bCtx.deriveSecret();
+ // Optional DH sanity check (only when both sides are DH keys).
+ try {
+ assertDhCompatible(alice.getPrivate(), bob.getPublic());
+ assertDhCompatible(bob.getPrivate(), alice.getPublic());
+ } catch (Throwable ignore) {
+ // not DH, or provider-specific checks not applicable; continue anyway
+ }
- System.out.println(" Alice.secret =" + hex(zA));
- System.out.println(" Bob.secret =" + hex(zB));
-
- assertArrayEquals(zA, zB, alg.id() + ": DH/XDH secrets mismatch");
- System.out.println("...ok");
+ AgreementContext aCtx = null;
+ AgreementContext bCtx = null;
try {
- aCtx.close();
- } catch (IOException ignore) {
- }
- try {
- bCtx.close();
- } catch (IOException ignore) {
+ aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(), spec);
+ bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), spec);
+
+ aCtx.setPeerPublic(bob.getPublic());
+ bCtx.setPeerPublic(alice.getPublic());
+
+ byte[] zA = aCtx.deriveSecret();
+ byte[] zB = bCtx.deriveSecret();
+
+ System.out.println("...Alice.secret =" + hex(zA));
+ System.out.println("...Bob.secret =" + hex(zB));
+
+ assertArrayEquals(zA, zB, alg.id() + ": CLASSIC_AGREEMENT secrets mismatch");
+ System.out.println("...CLASSIC_AGREEMENT...ok");
+ } finally {
+ if (aCtx != null) {
+ try {
+ aCtx.close();
+ } catch (Exception ignored) {
+ // ignore
+ }
+ }
+ if (bCtx != null) {
+ try {
+ bCtx.close();
+ } catch (Exception ignored) {
+ // ignore
+ }
+ }
}
+
+ // do not break: a single algorithm may have multiple agreement capabilities
}
}
}
@@ -271,19 +404,17 @@ public class AgreementAlgorithmsRoundTripTest {
private static KeyPair generateKeyPair(CryptoAlgorithm alg) {
try {
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
- // System.out.println(" ...keyPair " + bi.getClass().getName());
if (bi.defaultKeySpec == null) {
- // System.out.println(" ......skip default = null --> it was for keyImport");
continue;
}
@SuppressWarnings("unchecked")
Class specType = (Class) bi.specType;
- AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(specType);
+ AsymmetricKeyBuilder builder = alg.asymmetricKeyBuilder(specType);
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
- // System.out.println(" ......generated from " + spec);
- return b.generateKeyPair(spec);
+ return builder.generateKeyPair(spec);
}
} catch (Throwable ignore) {
+ // ignore
}
return null;
}
@@ -300,6 +431,7 @@ public class AgreementAlgorithmsRoundTripTest {
}
}
} catch (Throwable ignore) {
+ // ignore
}
return null;
}
@@ -315,15 +447,13 @@ public class AgreementAlgorithmsRoundTripTest {
DHPrivateKey dhPriv = (DHPrivateKey) priv;
DHPublicKey dhPub = (DHPublicKey) pub;
- // 1) Parameter compatibility: same p,g and l-compatible (0 means "unspecified")
DHParameterSpec a = dhPriv.getParams();
DHParameterSpec b = dhPub.getParams();
if (a == null || b == null) {
throw new InvalidKeyException("Missing DH parameters on one of the keys");
}
if (!a.getP().equals(b.getP()) || !a.getG().equals(b.getG())) {
- System.out.printf(" ...a.p=%s%n ...b.p=%s%n ...a.g=%s%n ...b.g=%s%n", a.getP(), b.getP(), a.getG(),
- b.getG());
+ System.out.printf("...a.p=%s%n...b.p=%s%n...a.g=%s%n...b.g=%s%n", a.getP(), b.getP(), a.getG(), b.getG());
throw new InvalidKeyException("Incompatible DH parameters: (p,g) differ");
}
int la = a.getL();
@@ -333,8 +463,6 @@ public class AgreementAlgorithmsRoundTripTest {
"Incompatible DH parameters: private value length (l) differs: " + la + " vs " + lb);
}
- // 2) Public value Y sanity check: 2 <= Y <= p-2 (reject trivial/small subgroup
- // values)
BigInteger p = a.getP();
BigInteger y = dhPub.getY();
if (y == null) {
@@ -345,12 +473,10 @@ public class AgreementAlgorithmsRoundTripTest {
throw new InvalidKeyException("DH public value Y out of range");
}
- // 3) Trial doPhase to prove the pair actually works with the provider
KeyAgreement ka = KeyAgreement.getInstance("DiffieHellman");
try {
ka.init(priv);
- ka.doPhase(pub, true); // if this throws, they are not operationally compatible
- // (we don't need the secret here; just proving doPhase succeeds)
+ ka.doPhase(pub, true);
} catch (InvalidKeyException e) {
throw new InvalidKeyException("KeyAgreement.doPhase failed: " + e.getMessage(), e);
}
diff --git a/samples/src/test/java/demo/AgreementVariantsTest.java b/samples/src/test/java/demo/AgreementVariantsTest.java
new file mode 100644
index 0000000..096d38a
--- /dev/null
+++ b/samples/src/test/java/demo/AgreementVariantsTest.java
@@ -0,0 +1,308 @@
+/*******************************************************************************
+ * Copyright (C) 2025, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. All advertising materials mentioning features or use of this software must
+ * display the following acknowledgement:
+ * This product includes software developed by the Egothor project.
+ *
+ * 4. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ******************************************************************************/
+package demo;
+
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import zeroecho.core.CryptoAlgorithm;
+import zeroecho.core.CryptoAlgorithms;
+import zeroecho.core.KeyUsage;
+import zeroecho.core.alg.common.agreement.KeyPairKey;
+import zeroecho.core.alg.kyber.KyberKeyGenSpec;
+import zeroecho.core.alg.xdh.XdhSpec;
+import zeroecho.core.context.AgreementContext;
+import zeroecho.core.context.MessageAgreementContext;
+import zeroecho.core.spec.VoidSpec;
+import zeroecho.core.util.Strings;
+import zeroecho.sdk.util.BouncyCastleActivator;
+
+/**
+ * Demonstration of agreement usage variants in ZeroEcho.
+ *
+ *
+ * This sample illustrates three complementary models used in practice:
+ *
+ *
+ * - KEM_ADAPTER (example: ML-KEM / Kyber): the initiator is
+ * constructed with the recipient's {@link PublicKey} and produces an outbound
+ * encapsulation message; the responder is constructed with the matching
+ * {@link PrivateKey} and consumes that message.
+ * - CLASSIC_AGREEMENT (example: XDH / X25519): both parties hold their
+ * own {@link PrivateKey}, set the peer {@link PublicKey} explicitly, and derive
+ * the same raw shared secret.
+ * - PAIR_MESSAGE (example: XDH / X25519): both parties hold a key pair
+ * and exchange messages that are simply the X.509 SPKI encodings of their
+ * public keys. This yields a message-oriented handshake without changing the
+ * underlying agreement primitive.
+ *
+ *
+ * Important note
+ *
+ * All examples below produce a raw agreement secret (the direct output
+ * of KEM decapsulation or Diffie–Hellman agreement). Real protocols should feed
+ * the raw secret into a suitable KDF (typically HKDF) together with
+ * transcript/context info before using it as key material.
+ *
+ *
+ * Note on resource management
+ *
+ * The examples in this class intentionally do not use the
+ * {@code try-with-resources} construct when working with
+ * {@link zeroecho.core.context.AgreementContext} and
+ * {@link zeroecho.core.context.MessageAgreementContext}.
+ *
+ *
+ *
+ * Agreement contexts represent protocol-level state rather than traditional I/O
+ * resources. In real-world applications their lifecycle often spans multiple
+ * protocol steps (message send, receive, validation, key derivation) and may
+ * cross method or thread boundaries. Using explicit {@code try/finally} blocks
+ * in the examples makes this lifecycle visible and closer to how agreement
+ * contexts are typically managed in production code.
+ *
+ *
+ *
+ * In short-lived, fully synchronous scenarios (such as unit tests),
+ * {@code try-with-resources} is perfectly acceptable. It is omitted here purely
+ * for didactic reasons.
+ *
+ */
+class AgreementVariantsTest {
+
+ private static final Logger LOG = Logger.getLogger(AgreementVariantsTest.class.getName());
+
+ @BeforeAll
+ static void setup() {
+ // Optional: activate BC/BCPQC if present.
+ // Keeps tests runnable even when providers are missing.
+ try {
+ BouncyCastleActivator.init();
+ } catch (Throwable ignore) {
+ // ignore
+ }
+ }
+
+ /**
+ * KEM_ADAPTER example for ML-KEM (Kyber):
+ *
+ *
+ * This models a common "send one message, derive shared secret" pattern:
+ *
+ *
+ * - Initiator uses recipient {@link PublicKey} and produces an encapsulation
+ * message.
+ * - Responder uses recipient {@link PrivateKey}, consumes the message, and
+ * derives the same secret.
+ *
+ */
+ @Test
+ void kemAdapter_mlKem_roundTrip() throws Exception {
+ LOG.info("kemAdapter_mlKem_roundTrip - KEM_ADAPTER (ML-KEM)");
+
+ KeyPair recipient = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024());
+
+ MessageAgreementContext initiator = null;
+ MessageAgreementContext responder = null;
+ try {
+ // Initiator: constructed with recipient's public key (encapsulation side).
+ initiator = CryptoAlgorithms.create("ML-KEM", KeyUsage.AGREEMENT, recipient.getPublic(), VoidSpec.INSTANCE);
+
+ // Responder: constructed with recipient's private key (decapsulation side).
+ responder = CryptoAlgorithms.create("ML-KEM", KeyUsage.AGREEMENT, recipient.getPrivate(),
+ VoidSpec.INSTANCE);
+
+ // One-shot outbound message: KEM ciphertext / encapsulation payload.
+ byte[] enc = initiator.getPeerMessage();
+ // Responder consumes ciphertext to derive its secret.
+ responder.setPeerMessage(enc);
+
+ byte[] s1 = initiator.deriveSecret();
+ byte[] s2 = responder.deriveSecret();
+
+ LOG.log(Level.INFO, "KEM_ADAPTER: ciphertext={0}", Strings.toShortHexString(enc));
+ LOG.log(Level.INFO, "KEM_ADAPTER: initiatorSecret={0}", Strings.toShortHexString(s1));
+ LOG.log(Level.INFO, "KEM_ADAPTER: responderSecret={0}", Strings.toShortHexString(s2));
+ LOG.log(Level.INFO, "KEM_ADAPTER: secretsEqual={0}", Boolean.valueOf(Arrays.equals(s1, s2)));
+ } finally {
+ if (initiator != null) {
+ try {
+ initiator.close();
+ } catch (Exception ignore) {
+ // ignore
+ }
+ }
+ if (responder != null) {
+ try {
+ responder.close();
+ } catch (Exception ignore) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * CLASSIC_AGREEMENT example for XDH/X25519:
+ *
+ *
+ * This is the traditional Diffie–Hellman model: both parties generate their own
+ * key pair, each side keeps a private key, and the peer public key is provided
+ * out-of-band (protocol message or session state).
+ *
+ */
+ @Test
+ void classicAgreement_x25519_roundTrip() throws Exception {
+ LOG.info("classicAgreement_x25519_roundTrip - CLASSIC_AGREEMENT (X25519)");
+
+ CryptoAlgorithm xdh = CryptoAlgorithms.require("Xdh");
+ KeyPair alice = xdh.generateKeyPair();
+ KeyPair bob = xdh.generateKeyPair();
+
+ AgreementContext aCtx = null;
+ AgreementContext bCtx = null;
+
+ try {
+ // Both contexts are built from local private keys.
+ aCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, alice.getPrivate(), XdhSpec.X25519);
+ bCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, bob.getPrivate(), XdhSpec.X25519);
+
+ // The protocol layer provides peer public keys (here we use in-memory
+ // exchange).
+ aCtx.setPeerPublic(bob.getPublic());
+ bCtx.setPeerPublic(alice.getPublic());
+
+ byte[] s1 = aCtx.deriveSecret();
+ byte[] s2 = bCtx.deriveSecret();
+
+ LOG.log(Level.INFO, "CLASSIC_AGREEMENT: aliceSecret={0}", Strings.toShortHexString(s1));
+ LOG.log(Level.INFO, "CLASSIC_AGREEMENT: bobSecret={0}", Strings.toShortHexString(s2));
+ LOG.log(Level.INFO, "CLASSIC_AGREEMENT: secretsEqual={0}", Boolean.valueOf(Arrays.equals(s1, s2)));
+ } finally {
+ if (aCtx != null) {
+ try {
+ aCtx.close();
+ } catch (Exception ignore) {
+ // ignore
+ }
+ }
+ if (bCtx != null) {
+ try {
+ bCtx.close();
+ } catch (Exception ignore) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * PAIR_MESSAGE example for XDH/X25519:
+ *
+ *
+ * This demonstrates the "message-oriented" handshake for DH-style agreements.
+ * Each party holds a key pair and the outbound message is simply the local
+ * public key encoding (SPKI). The receiver imports the encoding and binds it as
+ * the peer key.
+ *
+ *
+ *
+ * This model is particularly practical for protocol implementations because it
+ * makes the "to-be-sent" artifact explicit (a byte array message), similarly to
+ * KEM ciphertexts.
+ *
+ */
+ @Test
+ void pairMessage_x25519_roundTrip() throws Exception {
+ LOG.info("pairMessage_x25519_roundTrip - PAIR_MESSAGE (X25519)");
+
+ CryptoAlgorithm xdh = CryptoAlgorithms.require("Xdh");
+ KeyPair alice = xdh.generateKeyPair();
+ KeyPair bob = xdh.generateKeyPair();
+
+ // Wrapper is required because ZeroEcho capability dispatch uses Key (KeyPair is
+ // not a Key).
+ KeyPairKey aliceKey = new KeyPairKey(alice);
+ KeyPairKey bobKey = new KeyPairKey(bob);
+
+ MessageAgreementContext aCtx = null;
+ MessageAgreementContext bCtx = null;
+
+ try {
+ aCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, aliceKey, XdhSpec.X25519);
+ bCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, bobKey, XdhSpec.X25519);
+
+ // Outbound messages: SPKI encodings of local public keys.
+ byte[] aMsg = aCtx.getPeerMessage();
+ byte[] bMsg = bCtx.getPeerMessage();
+
+ LOG.log(Level.INFO, "PAIR_MESSAGE: aliceMsg={0}", Strings.toShortHexString(aMsg));
+ LOG.log(Level.INFO, "PAIR_MESSAGE: bobMsg={0}", Strings.toShortHexString(bMsg));
+
+ // Each side imports peer public key from message.
+ aCtx.setPeerMessage(bMsg);
+ bCtx.setPeerMessage(aMsg);
+
+ byte[] s1 = aCtx.deriveSecret();
+ byte[] s2 = bCtx.deriveSecret();
+
+ LOG.log(Level.INFO, "PAIR_MESSAGE: aliceSecret={0}", Strings.toShortHexString(s1));
+ LOG.log(Level.INFO, "PAIR_MESSAGE: bobSecret={0}", Strings.toShortHexString(s2));
+ LOG.log(Level.INFO, "PAIR_MESSAGE: secretsEqual={0}", Boolean.valueOf(Arrays.equals(s1, s2)));
+ } finally {
+ if (aCtx != null) {
+ try {
+ aCtx.close();
+ } catch (Exception ignore) {
+ // ignore
+ }
+ }
+ if (bCtx != null) {
+ try {
+ bCtx.close();
+ } catch (Exception ignore) {
+ // ignore
+ }
+ }
+ }
+ }
+}