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: + *

+ * + * + *

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

+ *
    + *
  1. Create the context with the local key pair wrapper.
  2. + *
  3. Send {@link #getPeerMessage()} to the remote party.
  4. + *
  5. Receive the remote party's message and call + * {@link #setPeerMessage(byte[])}.
  6. + *
  7. Derive the raw shared secret with {@link #deriveSecret()}.
  8. + *
+ * + *

Security considerations

+ * + * + *

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 + } + } + } + } +}