/******************************************************************************* * Copyright (C) 2026, 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.Tag; import org.junit.jupiter.api.Test; 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.ZeroEchoSession; import zeroecho.sdk.util.BouncyCastleActivator; /** * Demonstration of agreement usage variants in ZeroEcho. * *

* This sample illustrates three complementary models used in practice: *

* * *

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. *

*/ @Tag("sample") class AgreementVariantsTest { private final ZeroEchoSession session = new ZeroEchoSession(); 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: *

* */ @Test void kemAdapter_mlKem_roundTrip() throws Exception { LOG.info("kemAdapter_mlKem_roundTrip - KEM_ADAPTER (ML-KEM)"); KeyPair recipient = session.keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024()); MessageAgreementContext initiator = null; MessageAgreementContext responder = null; try { // Initiator: constructed with recipient's public key (encapsulation side). initiator = session.createContext("ML-KEM", KeyUsage.AGREEMENT, recipient.getPublic(), VoidSpec.INSTANCE); // Responder: constructed with recipient's private key (decapsulation side). responder = session.createContext("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)"); KeyPair alice = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); KeyPair bob = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); AgreementContext aCtx = null; AgreementContext bCtx = null; try { // Both contexts are built from local private keys. aCtx = session.createContext("Xdh", KeyUsage.AGREEMENT, alice.getPrivate(), XdhSpec.X25519); bCtx = session.createContext("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)"); KeyPair alice = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); KeyPair bob = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // 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 = session.createContext("Xdh", KeyUsage.AGREEMENT, aliceKey, XdhSpec.X25519); bCtx = session.createContext("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 } } } } }