Initial commit (history reset)

This commit is contained in:
2025-09-16 23:14:24 +02:00
commit 2cc988925a
396 changed files with 71058 additions and 0 deletions

View File

@@ -0,0 +1,491 @@
/*******************************************************************************
* 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;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.SecretKey;
import javax.xml.XMLConstants;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import zeroecho.core.audit.JulAuditListenerStd;
import zeroecho.core.context.DigestContext;
import zeroecho.core.context.EncryptionContext;
import zeroecho.core.context.KemContext;
import zeroecho.core.context.MacContext;
import zeroecho.core.context.SignatureContext;
import zeroecho.core.io.TailStrippingInputStream;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.core.spi.SymmetricKeyBuilder;
import zeroecho.sdk.util.BouncyCastleActivator;
public class CatalogContractTest {
static Level effectiveLevel(Logger lg) {
for (Logger x = lg; x != null; x = x.getParent()) {
if (x.getLevel() != null) {
return x.getLevel();
}
}
return null;
}
static void dump(String name) {
Logger lg = Logger.getLogger(name);
System.out.println("[" + name + "] level=" + lg.getLevel() + " effective=" + effectiveLevel(lg) + " useParent="
+ lg.getUseParentHandlers());
for (Handler h : lg.getHandlers()) {
System.out.println(" handler=" + h.getClass().getSimpleName() + " level=" + h.getLevel());
}
}
@BeforeAll
public static void setupProvider() {
BouncyCastleActivator.init();
Logger jul = Logger.getLogger("zeroecho.audit");
jul.setLevel(Level.FINE); // see PROGRESS at FINE
CryptoAlgorithms.setAuditListener(JulAuditListenerStd.builder().logger(jul).infoLevel(Level.INFO)
.warnLevel(Level.WARNING).progressLevel(Level.FINE).includeStackTraces(true).build());
CryptoAlgorithms.setAuditMode(CryptoAlgorithms.AuditMode.WRAP);
dump("");
dump("zeroecho.core.audit");
}
private static void logBegin(Object... params) {
String thisClass = CatalogContractTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = CatalogContractTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static String prettyPrintXml(String xml) throws TransformerException {
TransformerFactory tf = TransformerFactory.newInstance();
tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
try {
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
} catch (IllegalArgumentException ignored) {
}
try {
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
} catch (IllegalArgumentException ignored) {
}
Transformer t = tf.newTransformer();
t.setOutputProperty(OutputKeys.METHOD, "xml");
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
t.setOutputProperty(OutputKeys.INDENT, "yes");
t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
StringWriter out = new StringWriter();
t.transform(new StreamSource(new StringReader(xml)), new StreamResult(out));
return out.toString();
}
// ---------- Tests ----------
@Test
void catalogSane() throws TransformerException {
logBegin();
CryptoCatalog cat = CryptoCatalog.load();
System.out.println(prettyPrintXml(cat.toXml()));
assertDoesNotThrow(cat::validate);
assertTrue(CryptoAlgorithms.available().size() > 0);
logEnd();
}
@Test
void genericRoundTrips() throws Exception {
logBegin();
byte[] msg = "roundtrip".getBytes();
CryptoAlgorithms.setAuditMode(CryptoAlgorithms.AuditMode.WRAP);
for (String id : CryptoAlgorithms.available()) {
CryptoAlgorithm alg = CryptoAlgorithms.require(id);
// SIGN/VERIFY (asymmetric)
if (alg.roles().contains(KeyUsage.SIGN) && alg.roles().contains(KeyUsage.VERIFY)
&& !alg.asymmetricBuildersInfo().isEmpty()) {
trySignVerify(id, msg);
System.out.println();
}
// ENCRYPT/DECRYPT
boolean hasSym = !alg.symmetricBuildersInfo().isEmpty();
boolean hasAsym = !alg.asymmetricBuildersInfo().isEmpty();
if (alg.roles().contains(KeyUsage.ENCRYPT) && alg.roles().contains(KeyUsage.DECRYPT)) {
if (hasSym || hasAsym) {
tryEncryptDecrypt(id, msg, hasSym, hasAsym);
System.out.println();
}
}
// KEM
if (alg.roles().contains(KeyUsage.ENCAPSULATE) && alg.roles().contains(KeyUsage.DECAPSULATE)
&& !alg.asymmetricBuildersInfo().isEmpty()) {
tryKem(id, msg);
System.out.println();
}
// DIGEST
if (alg.roles().contains(KeyUsage.DIGEST)) {
tryDigest(id, msg);
System.out.println();
}
// MAC (single role now; verification via setExpectedTag)
if (alg.roles().contains(KeyUsage.MAC) && !alg.symmetricBuildersInfo().isEmpty()) {
tryMac(id, msg);
System.out.println();
}
}
logEnd();
}
// ---------- Helpers: SIGN/VERIFY ----------
private void trySignVerify(String id, byte[] msg) throws Exception {
logBegin(id, Integer.valueOf(msg.length));
KeyPair kp = tryKeyPairWithDefaultSpec(CryptoAlgorithms.require(id));
if (kp == null) {
logEnd();
return;
}
// SIGN: produce [body][signature] and capture trailer
SignatureContext signer = CryptoAlgorithms.create(id, KeyUsage.SIGN, kp.getPrivate(), null);
final byte[][] sigHolder = new byte[1][];
final int sigLen = signer.tagLength();
try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) {
@Override
protected void processTail(byte[] tail) throws IOException {
sigHolder[0] = (tail == null ? null : tail.clone());
}
}) {
assertArrayEquals(msg, readAll(in));
} finally {
signer.close();
}
byte[] sig = sigHolder[0];
assertNotNull(sig, "signature missing");
assertTrue(sig.length > 0, "signature empty");
// VERIFY: supply signature via setExpectedTag and drain (throws on mismatch)
SignatureContext verifier = CryptoAlgorithms.create(id, KeyUsage.VERIFY, kp.getPublic(), null);
verifier.setExpectedTag(sig);
try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) {
readAll(verIn);
} finally {
verifier.close();
}
logEnd();
}
// ---------- Helpers: ENCRYPT/DECRYPT ----------
private void tryEncryptDecrypt(String id, byte[] msg, boolean hasSym, boolean hasAsym) throws Exception {
logBegin(id, Integer.valueOf(msg.length));
CryptoAlgorithm alg = CryptoAlgorithms.require(id);
if (hasSym) {
SecretKey sk = tryGenerateSecretWithDefaultSpec(alg);
if (sk != null) {
conflux.CtxInterface session = conflux.Ctx.INSTANCE.getContext("encdec-" + System.nanoTime());
EncryptionContext enc = CryptoAlgorithms.create(id, KeyUsage.ENCRYPT, sk, null);
if (enc instanceof zeroecho.core.spi.ContextAware ca) {
ca.setContext(session);
}
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
EncryptionContext dec = CryptoAlgorithms.create(id, KeyUsage.DECRYPT, sk, null);
if (dec instanceof zeroecho.core.spi.ContextAware ca2) {
ca2.setContext(session);
}
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
assertArrayEquals(msg, pt, "decrypt mismatch (symmetric) for " + id);
logEnd();
return;
}
}
if (hasAsym) {
KeyPair kp = tryKeyPairWithDefaultSpec(alg);
if (kp != null) {
EncryptionContext enc = CryptoAlgorithms.create(id, KeyUsage.ENCRYPT, kp.getPublic(), null);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
EncryptionContext dec = CryptoAlgorithms.create(id, KeyUsage.DECRYPT, kp.getPrivate(), null);
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
assertArrayEquals(msg, pt, "decrypt mismatch (asymmetric) for " + id);
}
}
logEnd();
}
// ---------- Helpers: KEM ----------
private void tryKem(String id, byte[] msg) throws Exception { // msg unused; kept for symmetry/logging
logBegin(id);
KeyPair kp = tryKeyPairWithDefaultSpec(CryptoAlgorithms.require(id));
if (kp == null) {
logEnd();
return;
}
KemContext pub = CryptoAlgorithms.create(id, KeyUsage.ENCAPSULATE, kp.getPublic(), null);
KemContext prv = CryptoAlgorithms.create(id, KeyUsage.DECAPSULATE, kp.getPrivate(), null);
KemContext.KemResult res = pub.encapsulate();
byte[] ss = prv.decapsulate(res.ciphertext());
assertArrayEquals(res.sharedSecret(), ss, "KEM shared secret mismatch");
pub.close();
prv.close();
logEnd();
}
// ---------- Helpers: DIGEST (streaming via trailer) ----------
private void tryDigest(String id, byte[] msg) throws Exception {
logBegin(id, Integer.valueOf(msg.length));
DigestContext dctx = CryptoAlgorithms.create(id, KeyUsage.DIGEST, NullKey.INSTANCE, null);
final byte[][] digestHolder = new byte[1][];
final int tagLen = dctx.tagLength();
// Produce digest as trailer and capture
try (InputStream in = new TailStrippingInputStream(dctx.wrap(new ByteArrayInputStream(msg)), tagLen, 8192) {
@Override
protected void processTail(byte[] tail) throws IOException {
digestHolder[0] = (tail == null ? null : tail.clone());
}
}) {
assertArrayEquals(msg, readAll(in)); // passthrough body unchanged
} finally {
dctx.close();
}
byte[] digest = digestHolder[0];
assertNotNull(digest);
assertTrue(digest.length > 0);
logEnd();
}
// ---------- Helpers: MAC (produce + verify) ----------
private void tryMac(String id, byte[] msg) throws Exception {
logBegin(id, Integer.valueOf(msg.length));
SecretKey sk = tryGenerateSecretWithDefaultSpec(CryptoAlgorithms.require(id));
if (sk == null) {
logEnd();
return;
}
// Produce tag: [body][tag], capture trailer
MacContext mac = CryptoAlgorithms.create(id, KeyUsage.MAC, sk, null);
final byte[][] tagHolder = new byte[1][];
final int tagLen = mac.tagLength();
try (InputStream in = new TailStrippingInputStream(mac.wrap(new ByteArrayInputStream(msg)), tagLen, 8192) {
@Override
protected void processTail(byte[] tail) throws IOException {
tagHolder[0] = (tail == null ? null : tail.clone());
}
}) {
assertArrayEquals(msg, readAll(in));
} finally {
mac.close();
}
byte[] tag = tagHolder[0];
assertNotNull(tag);
assertTrue(tag.length > 0);
// Verify: provide expected tag and drain (throws on mismatch)
MacContext ver = CryptoAlgorithms.create(id, KeyUsage.MAC, sk, null);
ver.setExpectedTag(tag);
try (InputStream in = ver.wrap(new ByteArrayInputStream(msg))) {
readAll(in);
} finally {
ver.close();
}
logEnd();
}
// ---------- Utility: readAll ----------
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
// ---------- Utility: discover default key material ----------
private KeyPair tryKeyPairWithDefaultSpec(CryptoAlgorithm alg) {
logBegin(alg.id());
try {
List<CryptoAlgorithm.AsymBuilderInfo> infos = alg.asymmetricBuildersInfo();
if (infos.isEmpty()) {
System.out.println("no asymmetric builder info");
logEnd();
return null;
}
for (CryptoAlgorithm.AsymBuilderInfo bi : infos) {
if (bi.defaultKeySpec == null) {
continue;
}
try {
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType;
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
AsymmetricKeyBuilder<AlgorithmKeySpec> builder = alg.asymmetricKeyBuilder(specType);
System.out.println("...building with " + specType.getName());
KeyPair kp = builder.generateKeyPair(spec);
if (kp != null) {
logEnd();
return kp;
}
} catch (UnsupportedOperationException e) {
// import-only
} catch (Throwable t) {
System.out.println("builder " + bi.specType.getSimpleName() + " failed to generate keypair: "
+ t.getClass().getSimpleName() + ": " + t.getMessage());
}
}
System.out.println("no builder with working default keygen");
logEnd();
return null;
} catch (Throwable t) {
System.out.println("...*** SKIP *** keyPair cannot be generated: " + t);
return null;
}
}
private SecretKey tryGenerateSecretWithDefaultSpec(CryptoAlgorithm alg) {
logBegin(alg.id());
try {
List<CryptoAlgorithm.SymBuilderInfo> infos = alg.symmetricBuildersInfo();
if (infos.isEmpty()) {
System.out.println("no symmetric builder info");
logEnd();
return null;
}
for (CryptoAlgorithm.SymBuilderInfo bi : infos) {
if (bi.defaultKeySpec() == null) {
continue;
}
try {
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType();
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec();
SymmetricKeyBuilder<AlgorithmKeySpec> builder = alg.symmetricKeyBuilder(specType);
SecretKey sk = builder.generateSecret(spec);
if (sk != null) {
logEnd();
return sk;
}
} catch (UnsupportedOperationException e) {
// import-only
} catch (Throwable t) {
System.out.println("symmetric builder " + bi.specType().getSimpleName()
+ " failed to generate secret: " + t.getClass().getSimpleName() + ": " + t.getMessage());
}
}
System.out.println("no builder with working default secret generation");
logEnd();
return null;
} catch (Throwable t) {
System.out.println("...*** SKIP *** secret cannot be generated: " + t);
return null;
}
}
}

View File

@@ -0,0 +1,176 @@
/*******************************************************************************
* 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.aes;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import conflux.Ctx;
import conflux.CtxInterface;
import zeroecho.core.ConfluxKeys;
import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.context.EncryptionContext;
import zeroecho.core.spi.ContextAware;
public class AesGcmCrossCheckTest {
// ---- logging helpers (same pattern you use elsewhere) ----
private static void logBegin(Object... params) {
String thisClass = AesGcmCrossCheckTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = AesGcmCrossCheckTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
@BeforeAll
static void setup() {
// add providers if needed
}
private static byte[] rand(int n) {
byte[] a = new byte[n];
new SecureRandom().nextBytes(a);
return a;
}
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
private static byte[] jcaGcmEncrypt(SecretKey key, byte[] iv, int tagBits, byte[] aad, byte[] msg)
throws Exception {
Cipher c = Cipher.getInstance("AES/GCM/NOPADDING");
c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(tagBits, iv));
if (aad != null && aad.length > 0) {
c.updateAAD(aad);
}
return c.doFinal(msg);
}
private static byte[] jcaGcmDecrypt(SecretKey key, byte[] iv, int tagBits, byte[] aad, byte[] ct) throws Exception {
Cipher c = Cipher.getInstance("AES/GCM/NOPADDING");
c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(tagBits, iv));
if (aad != null && aad.length > 0) {
c.updateAAD(aad);
}
return c.doFinal(ct);
}
@Test
void aesGcm_stream_vs_jca_ctxOnly_crosscheck() throws Exception {
final int SIZE = 32 * 1024 + 777;
final int TAG_BITS = 128;
logBegin(SIZE, "AES/GCM ctx-only cross-check");
// --- test vectors ---
byte[] msg = rand(SIZE);
byte[] iv = rand(12); // 12-byte IV for GCM
byte[] aad = "test-aad-123".getBytes();
// --- key (either via your builder or direct JCA; both fine) ---
CryptoAlgorithm aesAlg = CryptoAlgorithms.require("AES");
SecretKey key = aesAlg.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256());
// or:
// KeyGenerator kg = KeyGenerator.getInstance("AES");
// kg.init(256);
// SecretKey key = kg.generateKey();
// --- per-test context; store IV/AAD under the names AesCipherContext expects
// ---
CtxInterface session = Ctx.INSTANCE.getContext("aes-gcm-xchk-" + System.nanoTime());
session.put(ConfluxKeys.iv("AES"), iv);
session.put(ConfluxKeys.aad("AES"), aad);
AesSpec spec = AesSpec.gcm128(null);
// === STREAM ENCRYPT ===
EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(session);
byte[] ct_stream = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
// === JCA ENCRYPT (reference) ===
byte[] ct_jca = jcaGcmEncrypt(key, iv, TAG_BITS, aad, msg);
System.out.printf("ct_stream: %d bytes, ct_jca: %d bytes%n", ct_stream.length, ct_jca.length);
assertArrayEquals(ct_jca, ct_stream, "STREAM ciphertext != JCA ciphertext (IV/AAD/msg must match)");
// === STREAM DECRYPT of JCA ciphertext ===
EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec);
((ContextAware) dec1).setContext(session); // same IV/AAD in ctx
byte[] pt1 = readAll(dec1.attach(new ByteArrayInputStream(ct_jca)));
dec1.close();
assertArrayEquals(msg, pt1, "STREAM decrypt(JCA ct) mismatch");
// === JCA DECRYPT of STREAM ciphertext ===
byte[] pt2 = jcaGcmDecrypt(key, iv, TAG_BITS, aad, ct_stream);
assertArrayEquals(msg, pt2, "JCA decrypt(STREAM ct) mismatch");
logEnd();
}
}

View File

@@ -0,0 +1,192 @@
/*******************************************************************************
* 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.aes;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.SecretKey;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import conflux.Ctx;
import conflux.CtxInterface;
import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.context.EncryptionContext;
import zeroecho.core.spi.ContextAware;
public class AesLargeDataTest {
@BeforeAll
static void setup() {
// Providers if needed, e.g.:
// Security.addProvider(new BouncyCastleProvider());
}
private static void logBegin(Object... params) {
String thisClass = AesLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = AesLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static byte[] randomBytes(int len) {
byte[] data = new byte[len];
new SecureRandom().nextBytes(data);
return data;
}
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
/**
* AES-GCM round-trip using Ctx-only parameters (default; IV is shared in the
* session Ctx).
*/
@Test
void aesGcmLargeData_ctxOnly() throws Exception {
final int SIZE = 32 * 1024 + 777;
logBegin(SIZE, "AES/GCM/NOPADDING (Ctx-only)");
byte[] msg = randomBytes(SIZE);
CryptoAlgorithm aes = CryptoAlgorithms.require("AES");
SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256());
CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
AesSpec spec = AesSpec.gcm128(null);
EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(session);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec);
((ContextAware) dec).setContext(session);
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
assertArrayEquals(msg, pt, "AES-GCM (Ctx-only) mismatch");
logEnd();
}
/** AES-GCM round-trip using an in-band header (codec in Ctx). */
@Test
void aesGcmLargeData_headerCodec() throws Exception {
final int SIZE = 32 * 1024 + 777;
logBegin(SIZE, "AES/GCM/NOPADDING (HeaderCodec)");
byte[] msg = randomBytes(SIZE);
System.out.printf("...input: %d bytes%n", msg.length);
CryptoAlgorithm aes = CryptoAlgorithms.require("AES");
SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256());
CtxInterface session = Ctx.INSTANCE.getContext("aes-hdr-" + System.nanoTime());
AesSpec spec = AesSpec.gcm128(new AesHeaderCodec());
EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(session);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
System.out.printf("...encrypted: %d bytes%n", ct.length);
EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec);
((ContextAware) dec).setContext(session);
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
System.out.printf("...decrypted: %d bytes%n", pt.length);
assertArrayEquals(msg, pt, "AES-GCM (HeaderCodec) mismatch");
logEnd();
}
/** AES-CBC/PKCS7Padding round-trip (Ctx-only IV). */
@Test
void aesCbcPkcs5LargeData_ctxOnly() throws Exception {
final int SIZE = 32 * 1024 + 777;
logBegin(SIZE, "AES/CBC/PKCS7Padding (Ctx-only)");
byte[] msg = randomBytes(SIZE);
CryptoAlgorithm aes = CryptoAlgorithms.require("AES");
SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256());
CtxInterface session = Ctx.INSTANCE.getContext("aes-cbc-" + System.nanoTime());
AesSpec spec = AesSpec.cbcPkcs7(null);
EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(session);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec);
((ContextAware) dec).setContext(session);
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
assertArrayEquals(msg, pt, "AES-CBC decrypt mismatch");
logEnd();
}
}

View File

@@ -0,0 +1,329 @@
/*******************************************************************************
* 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.chacha;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.SecretKey;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import conflux.Ctx;
import conflux.CtxInterface;
import zeroecho.core.ConfluxKeys;
import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.context.EncryptionContext;
import zeroecho.core.spi.ContextAware;
/**
* Large-data round-trip tests for ChaCha20 and ChaCha20-Poly1305, mirroring the
* style of AesLargeDataTest.
*/
public class ChaChaLargeDataTest {
@BeforeAll
static void setup() {
// If you need external providers, add them here, e.g.:
// Security.addProvider(new BouncyCastleProvider());
}
private static void logBegin(Object... params) {
String thisClass = ChaChaLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = ChaChaLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static byte[] randomBytes(int len) {
byte[] data = new byte[len];
new SecureRandom().nextBytes(data);
return data;
}
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
// ------------------------------------------------------------------------
// ChaCha20 (stream)
// ------------------------------------------------------------------------
/** ChaCha20 round-trip using Ctx-only parameters (nonce shared via Ctx). */
@Test
void chacha20LargeData_ctxOnly() throws Exception {
final int SIZE = 32 * 1024 + 777;
logBegin(SIZE, "ChaCha20 (Ctx-only)");
byte[] msg = randomBytes(SIZE);
CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20");
SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
CtxInterface session = Ctx.INSTANCE.getContext("chacha-ctx-" + System.nanoTime());
ChaChaSpec spec = ChaChaSpec.builder().initialCounter(1).header(null).build();
EncryptionContext enc = CryptoAlgorithms.create("CHACHA20", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(session);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
EncryptionContext dec = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec);
((ContextAware) dec).setContext(session);
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
assertArrayEquals(msg, pt, "ChaCha20 (Ctx-only) mismatch");
logEnd();
}
/** ChaCha20 round-trip using an in-band header (nonce + counter in header). */
@Test
void chacha20LargeData_headerCodec() throws Exception {
final int SIZE = 32 * 1024 + 777;
logBegin(SIZE, "ChaCha20 (HeaderCodec)");
byte[] msg = randomBytes(SIZE);
CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20");
SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
CtxInterface session = Ctx.INSTANCE.getContext("chacha-hdr-" + System.nanoTime());
ChaChaSpec spec = ChaChaSpec.builder().initialCounter(1).header(new ChaChaHeaderCodec()).build();
EncryptionContext enc = CryptoAlgorithms.create("CHACHA20", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(session);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
EncryptionContext dec = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec);
((ContextAware) dec).setContext(session);
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
assertArrayEquals(msg, pt, "ChaCha20 (HeaderCodec) mismatch");
logEnd();
}
/**
* ChaCha20: counter affects the keystream. - Encrypt with counter=7. - Decrypt
* with the same counter → success. - Decrypt with a different counter →
* plaintext differs.
*/
@Test
void chacha20_counterMatters() throws Exception {
final int SIZE = 16 * 1024 + 3;
logBegin(SIZE, "ChaCha20 counter matters");
byte[] msg = randomBytes(SIZE);
CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20");
SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
// Encrypt with explicit counter=7 (in ctx), headerless (ctx-only).
CtxInterface encCtx = Ctx.INSTANCE.getContext("chacha-ctr-enc-" + System.nanoTime());
encCtx.put(ConfluxKeys.tagBits("CHACHA20"), 7);
ChaChaSpec spec = ChaChaSpec.builder().initialCounter(1).header(null).build();
EncryptionContext enc = CryptoAlgorithms.create("CHACHA20", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(encCtx);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
// Grab the generated nonce from the enc ctx to feed decrypt ctxs
byte[] nonce = encCtx.get(ConfluxKeys.iv("CHACHA20"));
// Correct decryption (counter=7)
CtxInterface decCtxOk = Ctx.INSTANCE.getContext("chacha-ctr-dec-ok-" + System.nanoTime());
decCtxOk.put(ConfluxKeys.iv("CHACHA20"), nonce);
decCtxOk.put(ConfluxKeys.tagBits("CHACHA20"), 7);
EncryptionContext decOk = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec);
((ContextAware) decOk).setContext(decCtxOk);
byte[] ptOk = readAll(decOk.attach(new ByteArrayInputStream(ct)));
decOk.close();
assertArrayEquals(msg, ptOk, "ChaCha20 decrypt with correct counter failed");
// Wrong decryption (counter=8) → plaintext must differ
CtxInterface decCtxBad = Ctx.INSTANCE.getContext("chacha-ctr-dec-bad-" + System.nanoTime());
decCtxBad.put(ConfluxKeys.iv("CHACHA20"), nonce);
decCtxBad.put(ConfluxKeys.tagBits("CHACHA20"), 8);
EncryptionContext decBad = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec);
((ContextAware) decBad).setContext(decCtxBad);
byte[] ptBad = readAll(decBad.attach(new ByteArrayInputStream(ct)));
decBad.close();
assertFalse(Arrays.equals(msg, ptBad), "ChaCha20 decrypt with wrong counter should not match");
logEnd();
}
// ------------------------------------------------------------------------
// ChaCha20-Poly1305 (AEAD)
// ------------------------------------------------------------------------
/** AEAD round-trip using Ctx-only parameters (nonce + AAD in Ctx). */
@Test
void chacha20Poly1305LargeData_ctxOnly_withAad() throws Exception {
final int SIZE = 32 * 1024 + 777;
logBegin(SIZE, "ChaCha20-Poly1305 (Ctx-only + AAD)");
byte[] msg = randomBytes(SIZE);
byte[] aad = "associated-data-ctx-only".getBytes();
CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305");
SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
CtxInterface session = Ctx.INSTANCE.getContext("chacha-aead-ctx-" + System.nanoTime());
session.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aad);
ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build();
EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(session);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
EncryptionContext dec = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec);
((ContextAware) dec).setContext(session);
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
assertArrayEquals(msg, pt, "ChaCha20-Poly1305 (Ctx-only + AAD) mismatch");
logEnd();
}
/** AEAD round-trip with in-band header; header validates AAD hash. */
@Test
void chacha20Poly1305LargeData_headerCodec_withAad() throws Exception {
final int SIZE = 32 * 1024 + 777;
logBegin(SIZE, "ChaCha20-Poly1305 (HeaderCodec + AAD)");
byte[] msg = randomBytes(SIZE);
byte[] aad = "associated-data-header".getBytes();
CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305");
SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
CtxInterface session = Ctx.INSTANCE.getContext("chacha-aead-hdr-" + System.nanoTime());
session.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aad);
ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(new ChaCha20Poly1305HeaderCodec()).build();
EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(session);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
EncryptionContext dec = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec);
((ContextAware) dec).setContext(session);
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
assertArrayEquals(msg, pt, "ChaCha20-Poly1305 (HeaderCodec + AAD) mismatch");
logEnd();
}
/**
* AEAD: AAD mismatch must fail verification (expect an IOException from
* doFinal).
*/
@Test
void chacha20Poly1305_aadMismatchFails() throws Exception {
final int SIZE = 8 * 1024 + 9;
logBegin(SIZE, "ChaCha20-Poly1305 AAD mismatch");
byte[] msg = randomBytes(SIZE);
byte[] aadEnc = "aad-enc".getBytes();
byte[] aadDec = "aad-dec-different".getBytes();
CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305");
SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
// Encrypt with AAD = aadEnc (ctx-only, no header)
CtxInterface encCtx = Ctx.INSTANCE.getContext("chacha-aead-enc-" + System.nanoTime());
encCtx.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aadEnc);
ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build();
EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec);
((ContextAware) enc).setContext(encCtx);
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
// Decrypt with different AAD → should fail during doFinal()
CtxInterface decCtx = Ctx.INSTANCE.getContext("chacha-aead-dec-" + System.nanoTime());
decCtx.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aadDec);
// copy the nonce from encCtx
byte[] nonce = encCtx.get(ConfluxKeys.iv("CHACHA20-POLY1305"));
decCtx.put(ConfluxKeys.iv("CHACHA20-POLY1305"), nonce);
EncryptionContext dec = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec);
((ContextAware) dec).setContext(decCtx);
assertThrows(IOException.class, () -> {
@SuppressWarnings("unused")
byte[] ignored = readAll(dec.attach(new ByteArrayInputStream(ct)));
dec.close();
}, "ChaCha20-Poly1305 should fail with AAD mismatch");
logEnd();
}
}

View File

@@ -0,0 +1,358 @@
/*******************************************************************************
* 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 static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.List;
import javax.crypto.KeyAgreement;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import zeroecho.core.Capability;
import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.context.AgreementContext;
import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spec.ContextSpec;
import zeroecho.core.spec.VoidSpec;
import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.sdk.util.BouncyCastleActivator;
public class AgreementAlgorithmsRoundTripTest {
// ----- logging helpers -----
private static void logBegin(Object... params) {
String thisClass = AgreementAlgorithmsRoundTripTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = AgreementAlgorithmsRoundTripTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static String hex(byte[] b) {
if (b == null) {
return "null";
}
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte v : b) {
if (sb.length() == 80) {
sb.append("...");
break;
}
if ((v & 0xFF) < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v & 0xFF));
}
return sb.toString();
}
private static String keyInfo(String who, Key key) {
if (key == null) {
return who + "=(null)";
}
String fmt = key.getFormat();
byte[] enc = key.getEncoded();
return who + "=(" + key.getAlgorithm() + ", format=" + fmt + ", encodedLen=" + (enc == null ? -1 : enc.length)
+ ")";
}
@BeforeAll
static void setup() {
// If needed, install/activate providers (e.g., BouncyCastlePQCProvider) here.
BouncyCastleActivator.init();
}
@Test
void runAllAgreementAlgos() throws Exception {
logBegin("AGREEMENT sweep");
for (String id : CryptoAlgorithms.available()) {
final CryptoAlgorithm alg;
try {
alg = CryptoAlgorithms.require(id);
} catch (Throwable t) {
continue;
}
final List<Capability> caps = alg.listCapabilities();
for (Capability cap : caps) {
if (cap.role() != KeyUsage.AGREEMENT) {
continue;
}
System.out.printf(" ...%s - AGREEMENT capability found", id);
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);
// AGREEMENT (KEM-style) via MessageAgreementContext adapter
if (MessageAgreementContext.class.isAssignableFrom(ctxType)) {
KeyPair bob = generateKeyPair(alg);
if (bob == null) {
System.out.println(" ...bob=null");
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) {
}
try {
bobCtx.close();
} catch (Exception ignored) {
}
break;
}
// -------- Classic DH/XDH: AgreementContext + real ContextSpec --------
else if (AgreementContext.class.isAssignableFrom(ctxType)
&& ContextSpec.class.isAssignableFrom(specType) && keyType == PrivateKey.class) {
KeyPair alice;
KeyPair bob;
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;
} catch (Throwable ignore) {
spec = tryExtractContextSpec(alg);
}
if (spec == null) {
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()));
// assertDhCompatible(alice.getPrivate(), bob.getPublic());
// assertDhCompatible(bob.getPrivate(), alice.getPublic());
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());
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() + ": DH/XDH secrets mismatch");
System.out.println("...ok");
try {
aCtx.close();
} catch (IOException ignore) {
}
try {
bCtx.close();
} catch (IOException ignore) {
}
}
}
}
logEnd();
}
// ----- helpers -----
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<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType;
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(specType);
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
// System.out.println(" ......generated from " + spec);
return b.generateKeyPair(spec);
}
} catch (Throwable ignore) {
}
return null;
}
private static ContextSpec tryExtractContextSpec(CryptoAlgorithm alg) {
try {
for (Capability c : alg.listCapabilities()) {
if (c.role() == KeyUsage.AGREEMENT && ContextSpec.class.isAssignableFrom(c.specType())) {
try {
return c.defaultSpec().get();
} catch (Throwable ignore) {
// continue searching
}
}
}
} catch (Throwable ignore) {
}
return null;
}
public static void assertDhCompatible(PrivateKey priv, PublicKey pub) throws GeneralSecurityException {
if (!(priv instanceof DHPrivateKey)) {
throw new InvalidKeyException("Private key is not DH: " + (priv == null ? "null" : priv.getAlgorithm()));
}
if (!(pub instanceof DHPublicKey)) {
throw new InvalidKeyException("Public key is not DH: " + (pub == null ? "null" : pub.getAlgorithm()));
}
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());
throw new InvalidKeyException("Incompatible DH parameters: (p,g) differ");
}
int la = a.getL();
int lb = b.getL();
if (la != 0 && lb != 0 && la != lb) {
throw new InvalidKeyException(
"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) {
throw new InvalidKeyException("DH public key has null Y");
}
BigInteger TWO = BigInteger.valueOf(2);
if (y.compareTo(TWO) < 0 || y.compareTo(p.subtract(TWO)) > 0) {
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)
} catch (InvalidKeyException e) {
throw new InvalidKeyException("KeyAgreement.doPhase failed: " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,215 @@
/*******************************************************************************
* 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.ecdsa;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.context.SignatureContext;
import zeroecho.core.io.TailStrippingInputStream;
public class EcdsaLargeDataTest {
@BeforeAll
static void setup() {
// No special provider needed for JDK 21+ SunEC.
}
private static void logBegin(Object... params) {
String thisClass = EcdsaLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + java.util.Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = EcdsaLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static byte[] randomBytes(int len) {
byte[] data = new byte[len];
new SecureRandom().nextBytes(data);
return data;
}
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
private static byte[] jcaEcdsaSign(String jcaName, PrivateKey priv, byte[] msg) throws Exception {
Signature s = Signature.getInstance(jcaName); // e.g. SHA256withECDSAinP1363Format
s.initSign(priv); // randomized ECDSA (fresh k)
int off = 0;
while (off < msg.length) {
int len = Math.min(8192, msg.length - off);
s.update(msg, off, len);
off += len;
}
return s.sign();
}
private static boolean jcaEcdsaVerify(String jcaName, PublicKey pub, byte[] msg, byte[] sig) throws Exception {
Signature s = Signature.getInstance(jcaName);
s.initVerify(pub);
int off = 0;
while (off < msg.length) {
int len = Math.min(8192, msg.length - off);
s.update(msg, off, len);
off += len;
}
return s.verify(sig);
}
private static void runCase(EcdsaCurveSpec spec, int size) throws Exception {
logBegin(spec.name(), Integer.valueOf(size), "ECDSA cross-check");
if (!CryptoAlgorithms.available().contains("ECDSA")) {
System.out.println("...*** SKIP *** ECDSA not registered");
return;
}
byte[] msg = randomBytes(size);
System.out.println("...msg=" + msg.length + " bytes");
// Key pair via your unified ECDSA algorithm and enum spec
KeyPair kp = CryptoAlgorithms.keyPair("ECDSA", spec);
// SIGN (streaming): emits [body][signature]; capture trailer
SignatureContext signer = CryptoAlgorithms.create("ECDSA", KeyUsage.SIGN, kp.getPrivate(), spec);
final byte[][] sigHolder = new byte[1][];
final int sigLen = signer.tagLength(); // should equal spec.signFixedLength()
byte[] sink1;
try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) {
@Override
protected void processTail(byte[] tail) throws IOException {
sigHolder[0] = (tail == null ? null : tail.clone());
}
}) {
sink1 = readAll(in);
} finally {
try {
signer.close();
} catch (Exception ignore) {
}
}
assertArrayEquals(msg, sink1, "sign passthrough mismatch");
byte[] ourSig = sigHolder[0];
assertNotNull(ourSig, "signature missing");
System.out.println("...signature size: " + ourSig.length + " (expected " + spec.signFixedLength() + ")");
// VERIFY with our streaming verifier
SignatureContext verifier = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec);
verifier.setExpectedTag(ourSig);
byte[] sink2;
try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) {
sink2 = readAll(verIn);
} finally {
try {
verifier.close();
} catch (Exception ignore) {
}
}
assertArrayEquals(msg, sink2, "verify passthrough mismatch");
System.out.println("...verified (our verifier on our signature): true");
// Cross-verify with JCA (P1363 format): JCA must accept our signature
assertTrue(jcaEcdsaVerify(spec.jcaFactory(), kp.getPublic(), msg, ourSig),
"JCA verify failed on our signature");
// Extra symmetry check (optional): our verifier must accept a JCA signature
// (different bytes)
byte[] jcaSig = jcaEcdsaSign(spec.jcaFactory(), kp.getPrivate(), msg);
SignatureContext verifier2 = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec);
verifier2.setExpectedTag(jcaSig);
try (InputStream verIn2 = verifier2.wrap(new ByteArrayInputStream(msg))) {
byte[] passthrough = readAll(verIn2);
assertArrayEquals(msg, passthrough, "our verifier failed on JCA signature");
} finally {
try {
verifier2.close();
} catch (Exception ignore) {
}
}
System.out.println("...verified (our verifier on JCA signature): true");
logEnd();
}
@Test
void ecdsa_p256_streaming_sign_verify_and_crosscheck_with_jca() throws Exception {
runCase(EcdsaCurveSpec.P256, 48 * 1024 + 123);
}
@Test
void ecdsa_p384_streaming_sign_verify_and_crosscheck_with_jca() throws Exception {
runCase(EcdsaCurveSpec.P384, 48 * 1024 + 127);
}
@Test
void ecdsa_p521_streaming_sign_verify_and_crosscheck_with_jca() throws Exception {
runCase(EcdsaCurveSpec.P512, 48 * 1024 + 131);
}
}

View File

@@ -0,0 +1,191 @@
/*******************************************************************************
* 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.ed25519;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.context.SignatureContext;
import zeroecho.core.io.TailStrippingInputStream;
public class Ed25519LargeDataTest {
@BeforeAll
static void setup() {
// Provider setup if needed
}
private static void logBegin(Object... params) {
String thisClass = Ed25519LargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + java.util.Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = Ed25519LargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static byte[] randomBytes(int len) {
byte[] data = new byte[len];
new SecureRandom().nextBytes(data);
return data;
}
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
private static byte[] jcaEd25519Sign(PrivateKey priv, byte[] msg) throws Exception {
Signature s = Signature.getInstance("Ed25519");
s.initSign(priv);
int off = 0;
while (off < msg.length) {
int len = Math.min(8192, msg.length - off);
s.update(msg, off, len);
off += len;
}
return s.sign();
}
private static boolean jcaEd25519Verify(PublicKey pub, byte[] msg, byte[] sig) throws Exception {
Signature s = Signature.getInstance("Ed25519");
s.initVerify(pub);
int off = 0;
while (off < msg.length) {
int len = Math.min(8192, msg.length - off);
s.update(msg, off, len);
off += len;
}
return s.verify(sig);
}
@Test
void ed25519_streaming_sign_verify_and_crosscheck_with_jca() throws Exception {
final int SIZE = 48 * 1024 + 123;
logBegin(Integer.valueOf(SIZE), "Ed25519 cross-check");
byte[] msg = randomBytes(SIZE);
if (!CryptoAlgorithms.available().contains("Ed25519")) {
System.out.println("...*** SKIP *** Ed25519 not registered");
return;
}
KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
// SIGN: context emits [body][signature] — capture trailer via
// TailStrippingInputStream
SignatureContext signer = CryptoAlgorithms.create("Ed25519", KeyUsage.SIGN, kp.getPrivate(), null);
final byte[][] sigHolder = new byte[1][];
final int sigLen = signer.tagLength();
byte[] sink1;
try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) {
@Override
protected void processTail(byte[] tail) throws IOException {
sigHolder[0] = (tail == null ? null : tail.clone());
}
}) {
sink1 = readAll(in);
} finally {
try {
signer.close();
} catch (Exception ignore) {
}
}
assertArrayEquals(msg, sink1, "sign passthrough mismatch");
byte[] ourSig = sigHolder[0];
assertNotNull(ourSig, "signature missing");
System.out.println("...input size: " + msg.length);
System.out.println("...signature size: " + ourSig.length);
// Cross-check with JCA
byte[] refSig = jcaEd25519Sign(kp.getPrivate(), msg);
assertArrayEquals(refSig, ourSig, "signature mismatch vs JCA reference");
// VERIFY: supply expected tag and drain (throws on mismatch)
SignatureContext verifier = CryptoAlgorithms.create("Ed25519", KeyUsage.VERIFY, kp.getPublic(), null);
verifier.setExpectedTag(ourSig);
byte[] sink2;
try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) {
sink2 = readAll(verIn);
} finally {
try {
verifier.close();
} catch (Exception ignore) {
}
}
assertArrayEquals(msg, sink2, "verify passthrough mismatch");
System.out.println("...verified: true");
// JCA verify on produced signature
assertTrue(jcaEd25519Verify(kp.getPublic(), msg, ourSig), "JCA verify failed on our signature");
logEnd();
}
}

View File

@@ -0,0 +1,191 @@
/*******************************************************************************
* 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.ed448;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.context.SignatureContext;
import zeroecho.core.io.TailStrippingInputStream;
public class Ed448LargeDataTest {
@BeforeAll
static void setup() {
// Provider setup if needed
}
private static void logBegin(Object... params) {
String thisClass = Ed448LargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + java.util.Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = Ed448LargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static byte[] randomBytes(int len) {
byte[] data = new byte[len];
new SecureRandom().nextBytes(data);
return data;
}
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
private static byte[] jcaEd448Sign(PrivateKey priv, byte[] msg) throws Exception {
Signature s = Signature.getInstance("Ed448");
s.initSign(priv);
int off = 0;
while (off < msg.length) {
int len = Math.min(8192, msg.length - off);
s.update(msg, off, len);
off += len;
}
return s.sign();
}
private static boolean jcaEd448Verify(PublicKey pub, byte[] msg, byte[] sig) throws Exception {
Signature s = Signature.getInstance("Ed448");
s.initVerify(pub);
int off = 0;
while (off < msg.length) {
int len = Math.min(8192, msg.length - off);
s.update(msg, off, len);
off += len;
}
return s.verify(sig);
}
@Test
void ed448_streaming_sign_verify_and_crosscheck_with_jca() throws Exception {
final int SIZE = 48 * 1024 + 123;
logBegin(Integer.valueOf(SIZE), "Ed448 cross-check");
byte[] msg = randomBytes(SIZE);
if (!CryptoAlgorithms.available().contains("Ed448")) {
System.out.println("...*** SKIP *** Ed448 not registered");
return;
}
KeyPair kp = CryptoAlgorithms.keyPair("Ed448", Ed448KeyGenSpec.defaultSpec());
// SIGN: context emits [body][signature] — capture trailer via
// TailStrippingInputStream
SignatureContext signer = CryptoAlgorithms.create("Ed448", KeyUsage.SIGN, kp.getPrivate(), null);
final byte[][] sigHolder = new byte[1][];
final int sigLen = signer.tagLength();
byte[] sink1;
try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) {
@Override
protected void processTail(byte[] tail) throws IOException {
sigHolder[0] = (tail == null ? null : tail.clone());
}
}) {
sink1 = readAll(in);
} finally {
try {
signer.close();
} catch (Exception ignore) {
}
}
assertArrayEquals(msg, sink1, "sign passthrough mismatch");
byte[] ourSig = sigHolder[0];
assertNotNull(ourSig, "signature missing");
System.out.println("...input size: " + msg.length);
System.out.println("...signature size: " + ourSig.length);
// Cross-check with JCA
byte[] refSig = jcaEd448Sign(kp.getPrivate(), msg);
assertArrayEquals(refSig, ourSig, "signature mismatch vs JCA reference");
// VERIFY: supply expected tag and drain (throws on mismatch)
SignatureContext verifier = CryptoAlgorithms.create("Ed448", KeyUsage.VERIFY, kp.getPublic(), null);
verifier.setExpectedTag(ourSig);
byte[] sink2;
try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) {
sink2 = readAll(verIn);
} finally {
try {
verifier.close();
} catch (Exception ignore) {
}
}
assertArrayEquals(msg, sink2, "verify passthrough mismatch");
System.out.println("...verified: true");
// JCA verify on produced signature
assertTrue(jcaEd448Verify(kp.getPublic(), msg, ourSig), "JCA verify failed on our signature");
logEnd();
}
}

View File

@@ -0,0 +1,163 @@
/*******************************************************************************
* 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.elgamal;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.context.EncryptionContext;
public class ElgamalLargeDataTest {
@BeforeAll
static void setup() {
Security.addProvider(new BouncyCastleProvider());
}
private static void logBegin(Object... params) {
String thisClass = ElgamalLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = ElgamalLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static byte[] randomBytes(int len) {
byte[] data = new byte[len];
new SecureRandom().nextBytes(data);
return data;
}
/**
* PKCS1-style padding round-trip on >16KB payload (non-multiple of block size).
*/
@Test
void elgamalPkcs1LargeData() throws Exception {
final int SIZE = 32 * 1024 + 777; // >16KB, odd length to cross block boundaries
logBegin(SIZE, "PKCS1");
byte[] msg = randomBytes(SIZE);
System.out.printf("...input: %d bytes%n", msg.length);
KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
ElgamalEncSpec spec = ElgamalEncSpec.pkcs1();
EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec);
InputStream ctIn = enc.attach(new ByteArrayInputStream(msg));
byte[] ct = ctIn.readAllBytes();
enc.close();
System.out.printf("...encrypted: %d bytes%n", ct.length);
EncryptionContext dec = CryptoAlgorithms.create("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec);
InputStream ptIn = dec.attach(new ByteArrayInputStream(ct));
byte[] pt = ptIn.readAllBytes();
dec.close();
System.out.printf("...decrypted: %d bytes%n", pt.length);
assertArrayEquals(msg, pt, "ElGamal PKCS1 decrypt mismatch");
logEnd();
}
/** NOPADDING round-trip on >16KB payload (non-multiple of block size). */
@Test
void elgamalNoPaddingLargeData() throws Exception {
final int SIZE = 32 * 255 * 4;
logBegin(SIZE, "NOPADDING");
byte[] msg = randomBytes(SIZE);
System.out.printf("...input: %d bytes%n", msg.length);
KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
ElgamalEncSpec spec = ElgamalEncSpec.noPadding();
EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec);
InputStream ctIn = enc.attach(new ByteArrayInputStream(msg));
byte[] ct = ctIn.readAllBytes();
enc.close();
System.out.printf("...encrypted: %d bytes%n", ct.length);
EncryptionContext dec = CryptoAlgorithms.create("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec);
InputStream ptIn = dec.attach(new ByteArrayInputStream(ct));
byte[] pt = ptIn.readAllBytes();
dec.close();
System.out.printf("...decrypted: %d bytes%n", pt.length);
assertArrayEquals(msg, pt, "ElGamal NOPADDING decrypt mismatch");
logEnd();
}
/** NOPADDING round-trip on >16KB payload (non-multiple of block size). */
@Test
void elgamalNoPaddingFailLargeData() throws Exception {
final int SIZE = 32 * 255 * 4 + 3 /* + 777 */;
logBegin(SIZE, "NOPADDING");
byte[] msg = randomBytes(SIZE);
System.out.printf("...input: %d bytes%n", msg.length);
KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
ElgamalEncSpec spec = ElgamalEncSpec.noPadding();
EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec);
InputStream ctIn = enc.attach(new ByteArrayInputStream(msg));
assertThrowsExactly(IllegalStateException.class, () -> ctIn.readAllBytes(),
"No-padding cipher streams cannot processes incomplete blocks: 3 instead of 255");
logEnd();
}
}

View File

@@ -0,0 +1,181 @@
/*******************************************************************************
* 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.hmac;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
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.context.MacContext;
import zeroecho.core.io.TailStrippingInputStream;
public class HmacLargeDataTest {
@BeforeAll
static void setup() {
// register providers if needed
}
private static void logBegin(Object... params) {
String thisClass = HmacLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = HmacLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static byte[] randomBytes(int len) {
byte[] data = new byte[len];
new SecureRandom().nextBytes(data);
return data;
}
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
private static byte[] jcaMac(String alg, SecretKey key, byte[] msg) throws Exception {
Mac mac = Mac.getInstance(alg);
mac.init(key);
int off = 0;
while (off < msg.length) {
int len = Math.min(8192, msg.length - off);
mac.update(msg, off, len);
off += len;
}
return mac.doFinal();
}
@Test
void hmac_sha256_streaming_mac_and_verify_with_jca_crosscheck() throws Exception {
final int SIZE = 64 * 1024 + 321;
final String ALG_ID = "HMAC";
final String JCA_MAC = "HmacSHA256";
logBegin(Integer.valueOf(SIZE), JCA_MAC);
if (!CryptoAlgorithms.available().contains(ALG_ID)) {
System.out.println("...*** SKIP *** " + ALG_ID + " not registered");
return;
}
byte[] msg = randomBytes(SIZE);
System.out.println("...input size: " + msg.length);
CryptoAlgorithm algo = CryptoAlgorithms.require(ALG_ID);
// Generate a key (macName must match)
SecretKey key = algo.symmetricKeyBuilder(HmacKeyGenSpec.class).generateSecret(new HmacKeyGenSpec(JCA_MAC, 256));
// --- MAC (produce): engine emits [body][tag]; capture trailer ---
HmacSpec spec = HmacSpec.sha256();
MacContext mac = CryptoAlgorithms.create(ALG_ID, KeyUsage.MAC, key, spec);
final byte[][] tagHolder = new byte[1][];
final int tagLen = mac.tagLength();
byte[] pass1;
try (InputStream in = new TailStrippingInputStream(mac.wrap(new ByteArrayInputStream(msg)), tagLen, 8192) {
@Override
protected void processTail(byte[] tail) throws IOException {
tagHolder[0] = (tail == null ? null : tail.clone());
}
}) {
pass1 = readAll(in); // body only (trailer stripped)
} finally {
try {
mac.close();
} catch (Exception ignore) {
}
}
assertArrayEquals(msg, pass1, "MAC passthrough mismatch");
byte[] tag = tagHolder[0];
assertNotNull(tag, "streaming HMAC tag null");
assertTrue(tag.length > 0, "streaming HMAC tag empty");
System.out.println("...tag size: " + tag.length);
// Cross-check vs JCA
byte[] ref = jcaMac(JCA_MAC, key, msg);
assertArrayEquals(ref, tag, "HMAC tag mismatch vs JCA reference");
// --- VERIFY (consume): provide expected tag and drain (throws on mismatch) ---
MacContext ver = CryptoAlgorithms.create(ALG_ID, KeyUsage.MAC, key, spec);
ver.setExpectedTag(tag);
byte[] pass2;
try (InputStream in = ver.wrap(new ByteArrayInputStream(msg))) {
pass2 = readAll(in);
} finally {
try {
ver.close();
} catch (Exception ignore) {
}
}
assertArrayEquals(msg, pass2, "VERIFY passthrough mismatch");
System.out.println("...verified: true");
logEnd();
}
}

View File

@@ -0,0 +1,139 @@
/*******************************************************************************
* 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.rsa;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.SecureRandom;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.context.EncryptionContext;
public class RsaLargeDataTest {
@BeforeAll
static void setup() {
// If you register providers elsewhere, this can be empty.
// e.g., Security.addProvider(new BouncyCastleProvider());
}
private static void logBegin(Object... params) {
String thisClass = RsaLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = RsaLargeDataTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static byte[] randomBytes(int len) {
byte[] data = new byte[len];
new SecureRandom().nextBytes(data);
return data;
}
/** OAEP(SHA-256) round-trip on >16KB payload (non-multiple of block size). */
@Test
void rsaOaepLargeData() throws Exception {
final int SIZE = 32 * 1024 + 777; // >16KB, odd length to cross block boundaries
logBegin(SIZE, "OAEP(SHA-256)");
byte[] msg = randomBytes(SIZE);
System.out.printf("...input: %d bytes%n", msg.length);
KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
RsaEncSpec spec = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256);
EncryptionContext enc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec);
InputStream ctIn = enc.attach(new ByteArrayInputStream(msg));
byte[] ct = ctIn.readAllBytes();
enc.close();
System.out.printf("...encrypted: %d bytes%n", ct.length);
EncryptionContext dec = CryptoAlgorithms.create("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec);
InputStream ptIn = dec.attach(new ByteArrayInputStream(ct));
byte[] pt = ptIn.readAllBytes();
dec.close();
System.out.printf("...decrypted: %d bytes%n", pt.length);
assertArrayEquals(msg, pt, "RSA OAEP decrypt mismatch");
logEnd();
}
/** PKCS#1 v1.5 round-trip on >16KB payload (non-multiple of block size). */
@Test
void rsaPkcs1v15LargeData() throws Exception {
final int SIZE = 32 * 1024 + 777; // >16KB, odd length
logBegin(SIZE, "PKCS1v1.5");
byte[] msg = randomBytes(SIZE);
System.out.printf("...input: %d bytes%n", msg.length);
KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
RsaEncSpec spec = RsaEncSpec.pkcs1v15();
EncryptionContext enc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec);
InputStream ctIn = enc.attach(new ByteArrayInputStream(msg));
byte[] ct = ctIn.readAllBytes();
enc.close();
System.out.printf("...encrypted: %d bytes%n", ct.length);
EncryptionContext dec = CryptoAlgorithms.create("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec);
InputStream ptIn = dec.attach(new ByteArrayInputStream(ct));
byte[] pt = ptIn.readAllBytes();
dec.close();
System.out.printf("...decrypted: %d bytes%n", pt.length);
assertArrayEquals(msg, pt, "RSA PKCS1v1.5 decrypt mismatch");
logEnd();
}
}

View File

@@ -0,0 +1,140 @@
/*******************************************************************************
* 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.io;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.random.RandomGenerator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class TailStrippingInputStreamTest {
private static final int TAIL_LEN = 64;
private static final int IO_BUF = 4096;
@Test
@DisplayName("Short stream (<64 bytes) → body=0, tail=all")
void testShortStream() throws Exception {
final String method = "testShortStream(tailLen=" + TAIL_LEN + ", ioBuf=" + IO_BUF + ")";
System.out.println(method);
final byte[] input = randomBytes(37); // < 64
System.out.println("... input size: " + input.length);
final CapturingTailStream ts = new CapturingTailStream(new ByteArrayInputStream(input), TAIL_LEN, IO_BUF);
final byte[] body;
try (InputStream in = ts) {
body = in.readAllBytes();
System.out.println("... body size: " + body.length);
}
final byte[] tail = ts.getTail(); // printed at processTail() time
// Assertions
assertEquals(0, body.length, "Body must be empty when input < tailLen");
assertArrayEquals(input, tail, "Tail must equal the entire short input");
System.out.println("...ok");
}
@Test
@DisplayName("Large stream (>64 KiB) → body=input[0..n-tailLen), tail=last tailLen")
void testLargeStream() throws Exception {
final int size = 128 * 1024 + 5; // > 64 KiB
final String method = "testLargeStream(size=" + size + ", tailLen=" + TAIL_LEN + ", ioBuf=" + IO_BUF + ")";
System.out.println(method);
final byte[] input = randomBytes(size);
System.out.println("... input size: " + input.length);
final CapturingTailStream ts = new CapturingTailStream(new ByteArrayInputStream(input), TAIL_LEN, IO_BUF);
final byte[] body;
try (InputStream in = ts) {
body = in.readAllBytes();
System.out.println("... body size: " + body.length);
}
final byte[] tail = ts.getTail(); // printed at processTail() time
// Expected splits
final int bodyLen = input.length - TAIL_LEN;
final byte[] expectedBody = new byte[bodyLen];
final byte[] expectedTail = new byte[TAIL_LEN];
System.arraycopy(input, 0, expectedBody, 0, bodyLen);
System.arraycopy(input, bodyLen, expectedTail, 0, TAIL_LEN);
// Assertions
assertEquals(bodyLen, body.length, "Body length mismatch");
assertArrayEquals(expectedBody, body, "Body payload mismatch");
assertEquals(TAIL_LEN, tail.length, "Tail length mismatch");
assertArrayEquals(expectedTail, tail, "Tail payload mismatch");
System.out.println("...ok");
}
// ---------- Helpers ----------
private static byte[] randomBytes(int size) {
byte[] data = new byte[size];
RandomGenerator rng = RandomGenerator.of("L64X256MixRandom");
rng.nextBytes(data);
return data;
}
/**
* Test adapter that captures and prints the tail immediately when available.
*/
private static final class CapturingTailStream extends TailStrippingInputStream {
private byte[] tail;
CapturingTailStream(InputStream upstream, int tailLen, int ioBuffer) {
super(upstream, tailLen, ioBuffer);
}
@Override
protected void processTail(byte[] tail) throws IOException {
this.tail = tail;
System.out.println("... tail size: " + tail.length);
}
byte[] getTail() {
return tail == null ? new byte[0] : tail.clone();
}
}
}

View File

@@ -0,0 +1,167 @@
/*******************************************************************************
* 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.io;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.UUID;
import org.junit.jupiter.api.Test;
public class UtilTest {
@Test
public void testWriteAndReadUUID() throws IOException {
System.out.println("testWriteAndReadUUID");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
UUID original = UUID.randomUUID();
Util.write(baos, original);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
UUID result = Util.readUUID(bais);
assertEquals(original, result, "UUID should be preserved");
System.out.println("...ok");
}
@Test
public void testWriteAndReadUTF8() throws IOException {
System.out.println("testWriteAndReadUTF8");
String str = "Hello, world! こんにちは世界 🌍";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.writeUTF8(baos, str);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
String result = Util.readUTF8(bais, 1000);
assertEquals(str, result, "UTF8 string should be preserved");
System.out.println("...ok");
}
@Test
public void testWriteAndReadLong() throws IOException {
System.out.println("testWriteAndReadLong");
long value = 1234567890123456789L;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.writeLong(baos, value);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
long result = Util.readLong(bais);
assertEquals(value, result, "Long value should be preserved");
System.out.println("...ok");
}
@Test
public void testWriteAndReadByteArray() throws IOException {
System.out.println("testWriteAndReadByteArray");
byte[] data = new byte[256];
new Random().nextBytes(data);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.write(baos, data);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
byte[] result = Util.read(bais, 1000);
assertArrayEquals(data, result, "Byte array should be preserved");
System.out.println("...ok");
}
@Test
public void testReadWithMaxLengthExceeded() throws IOException {
System.out.println("testReadWithMaxLengthExceeded");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Write a packed integer length that's too large (e.g., 10_000_000)
Util.writePack7I(baos, 10_000_000);
baos.write(new byte[10]); // Minimal dummy data
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
IOException e = assertThrows(IOException.class, () -> Util.read(bais, 1000));
assertTrue(e.getMessage().contains("exceeds maximum allowed length"), "Expected length limit exception");
System.out.println("...ok");
}
@Test
public void testReadUTF8WithMaxLengthExceeded() throws IOException {
System.out.println("testReadUTF8WithMaxLengthExceeded");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.writePack7I(baos, 10_000_000);
baos.write("dummy".getBytes(StandardCharsets.UTF_8));
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
IOException e = assertThrows(IOException.class, () -> Util.readUTF8(bais, 1000));
assertTrue(e.getMessage().contains("exceeds maximum allowed length"), "Expected length limit exception");
System.out.println("...ok");
}
@Test
public void testPackedIntegerEncodingDecoding() throws IOException {
System.out.println("testPackedIntegerEncodingDecoding");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int original = 987654;
Util.writePack7I(baos, original);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
int result = Util.readPack7I(bais);
assertEquals(original, result, "Packed 7-bit integer should be preserved");
baos.reset();
long originalL = 9876543210L;
Util.writePack7L(baos, originalL);
bais = new ByteArrayInputStream(baos.toByteArray());
long resultL = Util.readPack7L(bais);
assertEquals(originalL, resultL, "Packed 7-bit long should be preserved");
System.out.println("...ok");
}
@Test
public void testReadEOFHandling() throws IOException {
System.out.println("testReadEOFHandling");
ByteArrayInputStream bais = new ByteArrayInputStream(new byte[3]);
assertThrows(EOFException.class, () -> Util.readLong(bais), "Expected EOFException for readLong");
System.out.println("...ok");
}
@Test
public void testWriteAndReadLargeData() throws IOException {
System.out.println("testWriteAndReadLargeData");
byte[] data = new byte[1024 * 1024]; // 1 MB
new Random().nextBytes(data);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.write(baos, data);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
byte[] result = Util.read(bais, data.length);
assertArrayEquals(data, result, "Large byte array should be preserved");
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,399 @@
/*******************************************************************************
* 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.storage;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.crypto.SecretKey;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.alg.rsa.RsaKeyGenSpec;
import zeroecho.core.alg.rsa.RsaPrivateKeySpec;
import zeroecho.core.alg.rsa.RsaPublicKeySpec;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.core.spi.SymmetricKeyBuilder;
import zeroecho.sdk.util.BouncyCastleActivator;
public class KeyringStoreDynamicTest {
@BeforeAll
static void setupProviders() {
BouncyCastleActivator.init();
}
private static void logBegin(Object... params) {
String thisClass = KeyringStoreDynamicTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = KeyringStoreDynamicTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
private static byte[] randomBytes(int len) {
byte[] b = new byte[len];
new SecureRandom().nextBytes(b);
return b;
}
private static String encLen(byte[] der) {
if (der == null) {
return "0";
}
String str = Base64.getEncoder().withoutPadding().encodeToString(der);
if (str.length() > 64) {
str = str.substring(0, 64) + "...";
}
return der.length + " / b64 " + str;
}
private static AlgorithmKeySpec makeImportSpec(Class<?> specType, byte[] material, String algId, Object defaultSpec)
throws Exception {
try {
Method m = specType.getMethod("fromRaw", byte[].class);
Object spec = m.invoke(null, material);
return (AlgorithmKeySpec) spec;
} catch (NoSuchMethodException ignored) {
}
try {
Method m = specType.getMethod("fromRaw", String.class, byte[].class);
String name = deriveVariantNameForImport(algId, defaultSpec);
Object spec = m.invoke(null, name, material);
return (AlgorithmKeySpec) spec;
} catch (NoSuchMethodException ignored) {
}
try {
Method m = specType.getMethod("of", byte[].class);
Object spec = m.invoke(null, material);
return (AlgorithmKeySpec) spec;
} catch (NoSuchMethodException ignored) {
}
try {
Constructor<?> c = specType.getConstructor(byte[].class);
Object spec = c.newInstance(new Object[] { material });
return (AlgorithmKeySpec) spec;
} catch (NoSuchMethodException ignored) {
}
try {
Constructor<?> c = specType.getConstructor(String.class);
Object spec = c.newInstance(Base64.getEncoder().encodeToString(material));
return (AlgorithmKeySpec) spec;
} catch (NoSuchMethodException ignored) {
}
throw new IllegalStateException("No usable import factory/ctor found for " + specType.getName());
}
private static String deriveVariantNameForImport(String algId, Object defaultSpec) {
if (defaultSpec != null) {
try {
Method m = defaultSpec.getClass().getMethod("macName");
Object v = m.invoke(defaultSpec);
if (v instanceof String) {
return (String) v;
}
} catch (Exception ignored) {
}
}
if ("HMAC".equalsIgnoreCase(algId)) {
return "HmacSHA256";
}
return algId;
}
private static boolean looksLikeImportSpecForPublic(Class<?> specType) {
String n = specType.getSimpleName();
return n.contains("Public") || n.endsWith("PublicKeySpec");
}
private static boolean looksLikeImportSpecForPrivate(Class<?> specType) {
String n = specType.getSimpleName();
return n.contains("Private") || n.endsWith("PrivateKeySpec");
}
private static boolean looksLikeImportSpecForSecret(Class<?> specType) {
String n = specType.getSimpleName();
return n.contains("Import") || n.endsWith("KeyImportSpec") || n.endsWith("SecretSpec");
}
@Test
void testExport(@TempDir Path tempDir) throws Exception {
logBegin();
Path keyringPath = tempDir.resolve("keyring-" + System.nanoTime() + ".txt");
KeyringStore store = new KeyringStore();
CryptoAlgorithm algo = CryptoAlgorithms.require("RSA");
KeyPair kp = algo.generateKeyPair(RsaKeyGenSpec.rsa4096());
store.putPrivate("alice.priv", "RSA", new RsaPrivateKeySpec(kp.getPrivate().getEncoded()));
store.putPublic("alice.pub", "RSA", new RsaPublicKeySpec(kp.getPublic().getEncoded()));
kp = algo.generateKeyPair(RsaKeyGenSpec.rsa4096());
store.putPrivate("bob.priv", "RSA", new RsaPrivateKeySpec(kp.getPrivate().getEncoded()));
store.putPublic("bob.pub", "RSA", new RsaPublicKeySpec(kp.getPublic().getEncoded()));
store.save(keyringPath);
store = KeyringStore.load(keyringPath);
String s = store.exportText(Collections.singleton("alice.pub"));
assertTrue(s.contains("# KeyringStore v1\n"));
assertTrue(s.contains("\n@entry\n"));
assertTrue(s.contains("\nalias=alice.pub\n"));
assertTrue(s.contains("\nalgorithm=RSA\n"));
assertTrue(s.contains("\nkind=PUBLIC_KEY\n"));
assertTrue(s.contains("\nspec=zeroecho.core.alg.rsa.RsaPublicKeySpec\n"));
assertTrue(s.contains("\ns.type=RSA-PUB\n"));
assertTrue(s.contains("\ns.x509.b64="));
logEnd();
}
@Test
void keyring_dynamic_population_roundtrip_and_dump(@TempDir Path tempDir) throws Exception {
logBegin();
KeyringStore store = new KeyringStore();
Set<String> ids = CryptoAlgorithms.available();
System.out.println("...algorithms discovered: " + ids);
int totalAdded = 0;
for (String id : ids) {
CryptoAlgorithm alg = CryptoAlgorithms.require(id);
System.out.println("\n-- " + id + " --");
if (!alg.asymmetricBuildersInfo().isEmpty()) {
int perAlg = 0;
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
if (bi.defaultKeySpec == null) {
continue;
}
try {
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> genSpecType = (Class<AlgorithmKeySpec>) bi.specType;
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(genSpecType);
AlgorithmKeySpec genSpec = (AlgorithmKeySpec) bi.defaultKeySpec;
KeyPair kp = b.generateKeyPair(genSpec);
PublicKey pub = kp.getPublic();
PrivateKey prv = kp.getPrivate();
Class<?> pubImpType = null;
Class<?> prvImpType = null;
for (CryptoAlgorithm.AsymBuilderInfo x : alg.asymmetricBuildersInfo()) {
if (looksLikeImportSpecForPublic(x.specType)) {
pubImpType = x.specType;
} else if (looksLikeImportSpecForPrivate(x.specType)) {
prvImpType = x.specType;
}
}
if (pubImpType != null) {
AlgorithmKeySpec pubSpec = makeImportSpec(pubImpType, pub.getEncoded(), id,
bi.defaultKeySpec);
String alias = id.toLowerCase() + "-pub-" + perAlg;
store.putPublic(alias, id, pubSpec);
System.out.println("..." + alias + " saved, len=" + encLen(pub.getEncoded()));
totalAdded++;
} else {
System.out.println("...*** SKIP *** no public import spec for " + id);
}
if (prvImpType != null) {
AlgorithmKeySpec prvSpec = makeImportSpec(prvImpType, prv.getEncoded(), id,
bi.defaultKeySpec);
String alias = id.toLowerCase() + "-prv-" + perAlg;
store.putPrivate(alias, id, prvSpec);
System.out.println("..." + alias + " saved, len=" + encLen(prv.getEncoded()));
totalAdded++;
} else {
System.out.println("...*** SKIP *** no private import spec for " + id);
}
perAlg++;
if (perAlg >= 3) {
break;
}
} catch (Throwable t) {
System.out.println("...*** SKIP asym for " + id + " *** " + t.getClass().getSimpleName() + ": "
+ t.getMessage());
}
}
}
if (!alg.symmetricBuildersInfo().isEmpty()) {
int perAlg = 0;
for (CryptoAlgorithm.SymBuilderInfo bi : alg.symmetricBuildersInfo()) {
if (bi.defaultKeySpec() == null) {
continue;
}
try {
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> genSpecType = (Class<AlgorithmKeySpec>) bi.specType();
SymmetricKeyBuilder<AlgorithmKeySpec> b = alg.symmetricKeyBuilder(genSpecType);
AlgorithmKeySpec genSpec = (AlgorithmKeySpec) bi.defaultKeySpec();
SecretKey sk = b.generateSecret(genSpec);
Class<?> impType = null;
for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) {
if (looksLikeImportSpecForSecret(x.specType())) {
impType = x.specType();
}
}
if (impType == null) {
for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) {
try {
x.specType().getConstructor(byte[].class);
impType = x.specType();
break;
} catch (NoSuchMethodException ignored) {
}
}
}
if (impType != null) {
byte[] raw = sk.getEncoded();
if (raw == null) {
raw = randomBytes(32);
}
AlgorithmKeySpec imp = makeImportSpec(impType, raw, id, bi.defaultKeySpec());
String alias = id.toLowerCase() + "-sec-" + perAlg;
store.putSecret(alias, id, imp);
System.out.println("..." + alias + " saved, len=" + (raw == null ? 0 : raw.length) + " "
+ Base64.getEncoder().withoutPadding().encodeToString(raw));
totalAdded++;
} else {
System.out.println("...*** SKIP *** no symmetric import spec for " + id);
}
perAlg++;
if (perAlg >= 3) {
break;
}
} catch (Throwable t) {
System.out.println("...*** SKIP sym for " + id + " *** " + t.getClass().getSimpleName() + ": "
+ t.getMessage());
}
}
}
}
// Persist using JUnit-managed temp directory
Path keyringPath = tempDir.resolve("keyring-" + System.nanoTime() + ".txt");
store.save(keyringPath);
System.out.println("\n...saved keyring: " + keyringPath.getFileName());
System.out.println("...entries stored: " + totalAdded);
KeyringStore loaded = KeyringStore.load(keyringPath);
assertTrue(loaded.aliases().size() >= Math.min(totalAdded, 1), "no entries reloaded");
int ok = 0;
for (String alias : loaded.aliases()) {
boolean success = false;
try {
PublicKey k = loaded.getPublic(alias);
if (k != null && k.getEncoded() != null) {
System.out.println("..." + alias + " OK public len=" + encLen(k.getEncoded()));
success = true;
}
} catch (Throwable ignore) {
}
if (!success) {
try {
PrivateKey k = loaded.getPrivate(alias);
if (k != null && k.getEncoded() != null) {
System.out.println("..." + alias + " OK private len=" + encLen(k.getEncoded()));
success = true;
}
} catch (Throwable ignore) {
}
}
if (!success) {
try {
SecretKey k = loaded.getSecret(alias);
if (k != null && k.getEncoded() != null) {
System.out.println("..." + alias + " OK secret len=" + encLen(k.getEncoded()));
success = true;
}
} catch (Throwable ignore) {
}
}
if (success) {
ok++;
} else {
System.out.println("...*** WARN *** could not reconstruct: " + alias);
}
}
assertTrue(ok > 0, "nothing reconstructed from keyring");
System.out.println("\n===== KEYRING DUMP BEGIN =====");
List<String> lines = Files.readAllLines(keyringPath, StandardCharsets.UTF_8);
for (String ln : lines) {
System.out.printf(ln.length() > 80 ? "%.77s...%n" : "%s%n", ln);
}
System.out.println("===== KEYRING DUMP END =====\n");
logEnd();
}
}

View File

@@ -0,0 +1,634 @@
/*******************************************************************************
* 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.sdk.builders;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.Signature;
import java.util.Arrays;
import java.util.Random;
import javax.crypto.SecretKey;
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.aes.AesKeyGenSpec;
import zeroecho.core.alg.digest.DigestSpec;
import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec;
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
import zeroecho.core.alg.rsa.RsaEncSpec;
import zeroecho.core.alg.rsa.RsaKeyGenSpec;
import zeroecho.core.alg.rsa.RsaSigSpec;
import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec;
import zeroecho.core.context.EncryptionContext;
import zeroecho.core.context.KemContext;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.core.spi.SymmetricKeyBuilder;
import zeroecho.core.tag.TagEngine;
import zeroecho.core.tag.TagEngineBuilder;
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
import zeroecho.sdk.builders.alg.KemDataContentBuilder;
import zeroecho.sdk.builders.alg.RsaEncDataContentBuilder;
import zeroecho.sdk.builders.core.DataContentBuilder;
import zeroecho.sdk.builders.core.DataContentChainBuilder;
import zeroecho.sdk.content.api.DataContent;
import zeroecho.sdk.content.api.PlainContent;
import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder;
import zeroecho.sdk.guard.UnlockMaterial;
import zeroecho.sdk.util.BouncyCastleActivator;
/**
* Round-trip tests for TagTrailerDataContentBuilder placed INSIDE AES, RSA, and
* KEM payloads using DataContentChainBuilder.
*
* Layout (ENCRYPT): Source -> TagTrailer(SHA-256) -> [AES|RSA|KEM payload]
* Layout (DECRYPT): Source -> [AES|RSA|KEM payload] -> TagTrailer(verify)
*/
public class TagTrailerDataContentBuilderTest {
// ---------- boilerplate logging ----------
private static void logBegin(Object... params) {
String thisClass = TagTrailerDataContentBuilderTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
}
private static void logEnd() {
String thisClass = TagTrailerDataContentBuilderTest.class.getName();
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
System.out.println(method + "...ok");
}
@BeforeAll
static void setup() {
// Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM,
// etc.)
try {
BouncyCastleActivator.init();
} catch (Throwable ignore) {
// keep tests runnable without BC if not present
}
}
// ---------- helpers ----------
private static byte[] random(int n) {
byte[] b = new byte[n];
new Random().nextBytes(b);
return b;
}
private static byte[] readAll(InputStream in) throws Exception {
try (in) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int r;
while ((r = in.read(buf)) != -1) {
out.write(buf, 0, r);
}
return out.toByteArray();
}
}
/** Minimal source builder for tests. */
private static final class BytesSourceBuilder implements DataContentBuilder<PlainContent> {
private final byte[] data;
private BytesSourceBuilder(byte[] data) {
this.data = data.clone();
}
static BytesSourceBuilder of(byte[] data) {
return new BytesSourceBuilder(data);
}
@Override
public PlainContent build(boolean encrypt) {
return new PlainContent() {
@Override
public void setInput(DataContent input) {
/* no upstream for sources */ }
@Override
public InputStream getStream() {
return new ByteArrayInputStream(data);
}
};
}
}
// ---------- AES/GCM with TagTrailer (Ed25519 SIGNATURE) ----------
@Test
void tag_inside_aes_gcm_with_ed25519_signature_roundtrip() throws Exception {
final int SIZE = 64 * 1024 + 11;
logBegin("AES/GCM + TagTrailer(Ed25519)", SIZE);
byte[] msg = random(SIZE);
System.out.println("...msg=" + msg.length + " bytes");
// AES key
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
.generateSecret(AesKeyGenSpec.aes256());
// Ed25519 keys (JCA)
KeyPair ed = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
TagEngine<Signature> tagEnc = TagEngineBuilder.ed25519Sign(ed.getPrivate()).get();
TagEngine<Signature> tagDec = TagEngineBuilder.ed25519Verify(ed.getPublic()).get();
// ENCRYPT: body -> [body||signature] -> AES-GCM
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
.add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build();
byte[] ct = readAll(enc.getStream());
System.out.println("...ct=" + ct.length + " bytes");
// DECRYPT: AES-GCM -> strip trailer -> verify Ed25519 at EOF
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader())
.add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build();
byte[] pt = readAll(dec.getStream());
assertArrayEquals(msg, pt, "AES/GCM tag-in-payload (Ed25519) roundtrip mismatch");
logEnd();
}
// ---------- AES/GCM with TagTrailer (SPHINCS+ SIGNATURE) ----------
@Test
void tag_inside_aes_gcm_with_sphincsplus_signature_roundtrip() throws Exception {
final int SIZE = 64 * 1024 + 17;
logBegin("AES/GCM + TagTrailer(SPHINCS+)", SIZE);
byte[] msg = random(SIZE);
System.out.println("...msg=" + msg.length + " bytes");
// AES key
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
.generateSecret(AesKeyGenSpec.aes256());
// SPHINCS+ key pair via registry (uses default param set from
// SphincsPlusKeyGenSpec)
KeyPair spx = CryptoAlgorithms.keyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec());
// Tag engines (SPHINCS+)
TagEngine<Signature> tagEnc = TagEngineBuilder.sphincsPlusSign(spx.getPrivate()).get();
TagEngine<Signature> tagDec = TagEngineBuilder.sphincsPlusVerify(spx.getPublic()).get();
// ENCRYPT: body -> [body||spxSig] -> AES-GCM
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
.add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build();
byte[] ct = readAll(enc.getStream());
System.out.println("...ct=" + ct.length + " bytes");
// DECRYPT: AES-GCM -> strip trailer -> verify SPHINCS+ at EOF
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader())
.add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build();
byte[] pt = readAll(dec.getStream());
assertArrayEquals(msg, pt, "AES/GCM tag-in-payload (SPHINCS+) roundtrip mismatch");
logEnd();
}
// ---------- AES/GCM with TagTrailer (RSA-PSS SIGNATURE) ----------
@Test
void tag_inside_aes_gcm_with_rsapss_signature_roundtrip() throws Exception {
final int SIZE = 96 * 1024 + 3;
logBegin("AES/GCM + TagTrailer(RSA-PSS)", SIZE);
byte[] msg = random(SIZE);
System.out.println("...msg=" + msg.length + " bytes");
// AES key
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
.generateSecret(AesKeyGenSpec.aes256());
// RSA-2048 keys (use registry for convenience)
KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
// Tag engines (SHA-256, saltLen=32)
RsaSigSpec pss = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32);
TagEngine<Signature> tagEnc = TagEngineBuilder.rsaSign(rsa.getPrivate(), pss).get();
TagEngine<Signature> tagDec = TagEngineBuilder.rsaVerify(rsa.getPublic(), pss).get();
// ENCRYPT: body -> [body||pssSig] -> AES-GCM
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
.add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build();
byte[] ct = readAll(enc.getStream());
System.out.println("...ct=" + ct.length + " bytes");
// DECRYPT: AES-GCM -> strip trailer -> verify PSS at EOF
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader())
.add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build();
byte[] pt = readAll(dec.getStream());
assertArrayEquals(msg, pt, "AES/GCM tag-in-payload (RSA-PSS) roundtrip mismatch");
logEnd();
}
// ---------- AES/GCM with TagTrailer (SHA-256) ----------
@Test
void tag_inside_aes_gcm_roundtrip() throws Exception {
final int SIZE = 48 * 1024 + 7;
logBegin("AES/GCM + TagTrailer", SIZE);
byte[] msg = random(SIZE);
System.out.println("...msg=" + msg.length + " bytes");
// AES key up-front so decrypt can reuse it
SymmetricKeyBuilder<AesKeyGenSpec> gen = CryptoAlgorithms.require("AES")
.symmetricKeyBuilder(AesKeyGenSpec.class);
SecretKey aesKey = gen.generateSecret(AesKeyGenSpec.aes256());
// ENCRYPT: [source] -> [tag trailer] -> [aes gcm]
DataContent encChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) // writes IV/AAD headers
// into stream
.build();
byte[] ciphertext = readAll(encChain.getStream());
System.out.println("...ct=" + ciphertext.length + " bytes");
// DECRYPT: [source(ct)] -> [aes gcm] -> [tag trailer verify]
DataContent decChain = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) // reads IV/AAD headers
// back
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)
.throwOnMismatch())
.build();
byte[] plain = readAll(decChain.getStream());
assertArrayEquals(msg, plain, "AES/GCM tag-in-payload roundtrip mismatch");
logEnd();
}
// ---------- RSA/OAEP with TagTrailer (keep body small; RSA is not a bulk
// stream cipher) ----------
@Test
void tag_inside_rsa_oaep_roundtrip() throws Exception {
final int SIZE = 96; // safe under RSA-2048 OAEP SHA-256 limit even with a 32-byte tag appended
logBegin("RSA/OAEP + TagTrailer", SIZE);
byte[] msg = "hello-".getBytes(StandardCharsets.UTF_8);
msg = Arrays.copyOf(msg, SIZE); // pad deterministic length for the test
System.out.println("...msg=" + msg.length + " bytes");
KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
// ENCRYPT: [source] -> [tag trailer] -> [rsa/oaep]
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192))
.add(RsaEncDataContentBuilder.builder().oaep(RsaEncSpec.Hash.SHA256).withPublicKey(kp.getPublic()))
.build();
byte[] ct = readAll(enc.getStream());
System.out.println("...ct=" + ct.length + " bytes");
// DECRYPT: [source(ct)] -> [rsa/oaep] -> [tag verify]
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct))
.add(RsaEncDataContentBuilder.builder().oaep(RsaEncSpec.Hash.SHA256).withPrivateKey(kp.getPrivate()))
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)
.throwOnMismatch())
.build();
byte[] pt = readAll(dec.getStream());
assertArrayEquals(msg, pt, "RSA/OAEP tag-in-payload roundtrip mismatch");
logEnd();
}
// ---------- KEM → AES/GCM payload with TagTrailer (skip if no KEM registered)
// ----------
@Test
void tag_inside_kem_payload_roundtrip() throws Exception {
final int SIZE = 32 * 1024 + 3;
logBegin("KEM→AES/GCM + TagTrailer", SIZE);
// Find a KEM we can use (e.g., "ML-KEM") with a working default spec.
String kemId = findKemIdOrNull();
if (kemId == null) {
System.out.println("...*** SKIP *** no KEM algorithm registered");
return;
}
CryptoAlgorithm kemAlg = CryptoAlgorithms.require(kemId);
KeyPair kemKeys = tryKeyPairWithDefaultSpec(kemAlg);
if (kemKeys == null) {
System.out.println("...*** SKIP *** cannot generate KEM key pair");
return;
}
byte[] msg = random(SIZE);
System.out.println("...msg=" + msg.length + " bytes");
// ENCRYPT: [source] -> [tag trailer] -> [KEM envelope with AES/GCM payload]
AesDataContentBuilder aesEnc = AesDataContentBuilder.builder().modeGcm(128) // 128-bit tag
.withHeader(); // carry IV etc.
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192))
.add(KemDataContentBuilder.builder().kem(kemId).recipientPublic(kemKeys.getPublic()).derivedKeyBytes(32) // AES-256
// key
// derived
// from
// KEM
// secret
.hkdfSha256("KEM-tag-demo".getBytes(java.nio.charset.StandardCharsets.US_ASCII))
.withAes(aesEnc))
.build();
byte[] envelope = readAll(enc.getStream());
System.out.println("...envelope=" + envelope.length + " bytes");
// DECRYPT: [source(envelope)] -> [KEM] -> [tag verify]
AesDataContentBuilder aesDec = AesDataContentBuilder.builder().modeGcm(128).withHeader();
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(envelope))
.add(KemDataContentBuilder.builder().kem(kemId).recipientPrivate(kemKeys.getPrivate())
.derivedKeyBytes(32)
.hkdfSha256("KEM-tag-demo".getBytes(java.nio.charset.StandardCharsets.US_ASCII))
.withAes(aesDec))
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)
.throwOnMismatch())
.build();
byte[] pt = readAll(dec.getStream());
assertArrayEquals(msg, pt, "KEM→GCM tag-in-payload roundtrip mismatch");
logEnd();
}
// -------------------------------------------------------------------------
// 1) AES-GCM payload, multi-recipient (RSA + Kyber + Password), tag trailer
// -------------------------------------------------------------------------
@Test
void tag_trailer_inside_multi_recipient_aes_gcm_rsa_kem_password_roundtrip() throws Exception {
final int SIZE = 96 * 1024 + 7;
final char[] PASSWORD = "correct horse battery staple".toCharArray();
logBegin(Integer.valueOf(SIZE), "AES-256/GCM + RSA + ML-KEM + password");
// message
byte[] msg = random(SIZE);
System.out.println("...input=" + msg.length);
// --- recipients ---
// RSA
KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
// ML-KEM (Kyber768 as a good mid-level)
KeyPair kem = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768());
// --- symmetric payload (AES-256/GCM, tag 128) ---
// IV length is handled internally (12 bytes for GCM) and persisted via header.
AesDataContentBuilder aesEnc = AesDataContentBuilder.builder().modeGcm(128).withHeader(); // write
// IV/tagBits/AAD-hash
// header for decrypt
// side
// --- tag trailer (SHA-256 digest as a trailer) ---
TagTrailerDataContentBuilder<byte[]> tagEnc = new TagTrailerDataContentBuilder<>(
TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192);
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kem.getPublic());
// --- envelope (ENCRYPT) with 3 recipients ---
MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesEnc)
// .addRsaOaepRecipient(rsa.getPublic()) // RSA-OAEP with SHA-256 MGF1
// .addKemRecipient("ML-KEM", kem.getPublic(), 32 /* kekBytes */, 16 /*
// hkdfSaltLen */)
.addRecipient(rsaEnc) // new API call
.addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32) // new API call
.addPasswordRecipient(PASSWORD, 120_000 /* pbkdf2 iters */, 16 /* saltLen */, 32 /* kekBytes */);
// Pipeline (encrypt): Bytes → Tag(trailer) → Multi-Recipient
DataContent encTail = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(tagEnc).add(envEnc)
.build();
byte[] encrypted = readAll(encTail.getStream());
System.out.println("...encrypted=" + encrypted.length);
// -------------- Decrypt three ways on the same ciphertext --------------
// a) by RSA private key
AesDataContentBuilder aesDecRsa = AesDataContentBuilder.builder().modeGcm(128).withHeader(); // read header to
// recover
// IV/tagBits
MultiRecipientDataSourceBuilder envDecRsa = new MultiRecipientDataSourceBuilder().withAes(aesDecRsa)
.unlockWith(new UnlockMaterial.Private(rsa.getPrivate()));
byte[] ptRsa = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)).add(envDecRsa)
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)
.throwOnMismatch())
.build().getStream());
System.out.println("...decrypted(RSA)=" + ptRsa.length);
assertArrayEquals(msg, ptRsa, "RSA path failed to recover the original");
// b) by KEM private key
AesDataContentBuilder aesDecKem = AesDataContentBuilder.builder().modeGcm(128).withHeader();
MultiRecipientDataSourceBuilder envDecKem = new MultiRecipientDataSourceBuilder().withAes(aesDecKem)
.unlockWith(new UnlockMaterial.Private(kem.getPrivate()));
byte[] ptKem = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)).add(envDecKem)
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)
.throwOnMismatch())
.build().getStream());
System.out.println("...decrypted(KEM)=" + ptKem.length);
assertArrayEquals(msg, ptKem, "KEM path failed to recover the original");
// c) by password
AesDataContentBuilder aesDecPwd = AesDataContentBuilder.builder().modeGcm(128).withHeader();
MultiRecipientDataSourceBuilder envDecPwd = new MultiRecipientDataSourceBuilder().withAes(aesDecPwd)
.unlockWith(new UnlockMaterial.Password(PASSWORD));
byte[] ptPwd = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)).add(envDecPwd)
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)
.throwOnMismatch())
.build().getStream());
System.out.println("...decrypted(PASSWORD)=" + ptPwd.length);
assertArrayEquals(msg, ptPwd, "Password path failed to recover the original");
logEnd();
}
// -------------------------------------------------------------------------
// 2) AES-CBC payload (PKCS#7), RSA recipient only, tag trailer for integrity
// -------------------------------------------------------------------------
@Test
void tag_trailer_inside_multi_recipient_aes_cbc_rsa_roundtrip() throws Exception {
final int SIZE = 48 * 1024 + 321;
logBegin(Integer.valueOf(SIZE), "AES-256/CBC + RSA");
byte[] msg = random(SIZE);
System.out.println("...input=" + msg.length);
KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
// AES-256/CBC with header so IV/params are serialized by the AES stage
AesDataContentBuilder aesCbc = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
TagTrailerDataContentBuilder<byte[]> tagEnc = new TagTrailerDataContentBuilder<>(
TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192);
TagTrailerDataContentBuilder<byte[]> tagDec = new TagTrailerDataContentBuilder<>(
TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)
// explicit for clarity
.throwOnMismatch();
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
// Envelope: recipient table (RSA-OAEP) + AES payload (CBC/PKCS7 with header)
MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesCbc)
// CEK length for AES-256
.payloadKeyBytes(32)
// .addRsaOaepRecipient(rsa.getPublic()); old API
.addRecipient(rsaEnc);
DataContent encTail = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
// append digest trailer BEFORE symmetric encryption
.add(tagEnc).add(envEnc).build();
byte[] encrypted = readAll(encTail.getStream());
System.out.println("...encrypted=" + encrypted.length);
MultiRecipientDataSourceBuilder envDec = new MultiRecipientDataSourceBuilder()
.withAes(AesDataContentBuilder.builder().modeCbcPkcs5()
// must match encrypt side
.withHeader())
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate()));
byte[] pt = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted))
// recover CEK via RSA-OAEP, then AES-CBC decrypt (reads header)
.add(envDec)
// strip & verify trailer
.add(tagDec).build().getStream());
System.out.println("...decrypted=" + pt.length);
assertArrayEquals(msg, pt);
logEnd();
}
// ---------- AES/GCM with TagTrailer (ECDSA P-256 SIGNATURE) ----------
@Test
void tag_inside_aes_gcm_with_ecdsa_p256_signature_roundtrip() throws Exception {
final int SIZE = 64 * 1024 + 5;
logBegin("AES/GCM + TagTrailer(ECDSA-P256)", SIZE);
byte[] msg = random(SIZE);
System.out.println("...msg=" + msg.length + " bytes");
// AES key
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
.generateSecret(AesKeyGenSpec.aes256());
// ECDSA P-256 keys (via your unified ECDSA algorithm)
KeyPair ecdsa = CryptoAlgorithms.keyPair("ECDSA", zeroecho.core.alg.ecdsa.EcdsaCurveSpec.P256);
// Tag engines (ECDSA/P-256 using P1363 format, fixed 64-byte tag)
TagEngine<Signature> tagEnc = TagEngineBuilder.ecdsaP256Sign(ecdsa.getPrivate()).get();
TagEngine<Signature> tagDec = TagEngineBuilder.ecdsaP256Verify(ecdsa.getPublic()).get();
// ENCRYPT: body -> [body||ecdsaSig] -> AES-GCM
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
.add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build();
byte[] ct = readAll(enc.getStream());
System.out.println("...ct=" + ct.length + " bytes");
// DECRYPT: AES-GCM -> strip trailer -> verify ECDSA at EOF
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct))
.add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader())
.add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build();
byte[] pt = readAll(dec.getStream());
assertArrayEquals(msg, pt, "AES/GCM tag-in-payload (ECDSA-P256) roundtrip mismatch");
logEnd();
}
// ---------- small registry helpers ----------
private static String findKemIdOrNull() {
for (String id : CryptoAlgorithms.available()) {
if (id.equalsIgnoreCase("ML-KEM") || id.equalsIgnoreCase("RSA-KEM") || id.toUpperCase().contains("KEM")) {
return id;
}
}
return null;
}
private static KeyPair tryKeyPairWithDefaultSpec(CryptoAlgorithm alg) {
try {
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
if (bi.defaultKeySpec == null) {
continue;
}
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType;
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(specType);
KeyPair kp = b.generateKeyPair(spec);
if (kp != null) {
return kp;
}
}
} catch (Throwable t) {
// fall through → return null
}
return null;
}
}

View File

@@ -0,0 +1,320 @@
/*******************************************************************************
* 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.sdk.builders.alg;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Stream;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.alg.bike.BikeKeyGenSpec;
import zeroecho.core.alg.cmce.CmceKeyGenSpec;
import zeroecho.core.alg.frodo.FrodoKeyGenSpec;
import zeroecho.core.alg.hqc.HqcKeyGenSpec;
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
import zeroecho.core.alg.ntru.NtruKeyGenSpec;
import zeroecho.core.alg.ntruprime.NtrulPrimeKeyGenSpec;
import zeroecho.core.alg.ntruprime.SntruPrimeKeyGenSpec;
import zeroecho.core.alg.saber.SaberKeyGenSpec;
import zeroecho.core.annotation.Describable;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.sdk.content.api.DataContent;
import zeroecho.sdk.content.api.PlainContent;
import zeroecho.sdk.util.Kdf;
/**
* Round-trip tests for KEM + symmetric envelope using the new
* {@link KemDataContentBuilder} that first configures the KEM and then
* delegates to an explicit AES/ChaCha builder.
*
* <p>
* This replaces the older ad-hoc payload builders (GcmBuilder/CcmBuilder/etc.).
* Modes exercised here are those implemented by {@link AesDataContentBuilder}
* and {@link ChaChaDataContentBuilder}: AES-GCM, AES-CBC, AES-CTR, and
* ChaCha20-Poly1305.
* </p>
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.MethodName.class)
class KemHybridRoundTripTest {
@BeforeAll
void setupProviders() {
// PQC providers (required by most KEM impls)
Security.addProvider(new BouncyCastlePQCProvider());
Security.addProvider(new BouncyCastleProvider());
}
// Only modes supported by the concrete AES/ChaCha builders
private static final String[] MODES = new String[] { "GCM", "CBC", "CTR", "CHACHA20-POLY1305" };
// Non-empty AAD to force AEAD path for ChaCha20-Poly1305 and also exercise GCM
// AAD
private static final byte[] AAD = new byte[] { 0x01 };
// Nicely print a spec
private static String labelOf(AlgorithmKeySpec spec) {
if (spec instanceof Describable d) {
return d.description();
}
return spec.getClass().getSimpleName();
}
// --- generic vectors over all KEMs we support (compile-time known) ---
static Stream<Arguments> vectors() {
List<Arguments> out = new ArrayList<>();
// 1) ML-KEM (Kyber)
var kybers = new KyberKeyGenSpec[] { KyberKeyGenSpec.kyber512(), KyberKeyGenSpec.kyber768(),
KyberKeyGenSpec.kyber1024() };
for (var spec : kybers) {
for (var m : MODES) {
out.add(Arguments.of("ML-KEM", spec, m, "ML-KEM:" + labelOf(spec) + " · " + m));
}
}
// 2) NTRU (HPS/HRSS)
var ntrus = new NtruKeyGenSpec[] { NtruKeyGenSpec.hps2048_509(), NtruKeyGenSpec.hps2048_677(),
NtruKeyGenSpec.hps4096_821(), NtruKeyGenSpec.hps4096_1229(), NtruKeyGenSpec.hrss701(),
NtruKeyGenSpec.hrss1373() };
for (var spec : ntrus) {
for (var m : MODES) {
out.add(Arguments.of("NTRU", spec, m, "NTRU:" + labelOf(spec) + " · " + m));
}
}
// 3) Classic McEliece (CMCE)
var cmces = new CmceKeyGenSpec[] { CmceKeyGenSpec.mceliece348864(), CmceKeyGenSpec.mceliece348864f(),
CmceKeyGenSpec.mceliece460896(), CmceKeyGenSpec.mceliece460896f(), CmceKeyGenSpec.mceliece6688128(),
CmceKeyGenSpec.mceliece6688128f(), CmceKeyGenSpec.mceliece6960119(), CmceKeyGenSpec.mceliece6960119f(),
CmceKeyGenSpec.mceliece8192128(), CmceKeyGenSpec.mceliece8192128f() };
for (var spec : cmces) {
for (var m : MODES) {
out.add(Arguments.of("CMCE", spec, m, "CMCE:" + labelOf(spec) + " · " + m));
}
}
// 4) FrodoKEM
var frodos = new FrodoKeyGenSpec[] { FrodoKeyGenSpec.frodo640aes(), FrodoKeyGenSpec.frodo640shake(),
FrodoKeyGenSpec.frodo976aes(), FrodoKeyGenSpec.frodo976shake(), FrodoKeyGenSpec.frodo1344aes(),
FrodoKeyGenSpec.frodo1344shake() };
for (var spec : frodos) {
for (var m : MODES) {
out.add(Arguments.of("Frodo", spec, m, "Frodo:" + labelOf(spec) + " · " + m));
}
}
// 5) SABER
var sabers = new SaberKeyGenSpec[] { SaberKeyGenSpec.lightsaberkem128r3(), SaberKeyGenSpec.saberkem128r3(),
SaberKeyGenSpec.firesaberkem128r3(), SaberKeyGenSpec.lightsaberkem192r3(),
SaberKeyGenSpec.saberkem192r3(), SaberKeyGenSpec.firesaberkem192r3(),
SaberKeyGenSpec.lightsaberkem256r3(), SaberKeyGenSpec.saberkem256r3(),
SaberKeyGenSpec.firesaberkem256r3() };
for (var spec : sabers) {
for (var m : MODES) {
out.add(Arguments.of("SABER", spec, m, "SABER:" + labelOf(spec) + " · " + m));
}
}
// 6) BIKE
var bikes = new BikeKeyGenSpec[] { BikeKeyGenSpec.bike128(), BikeKeyGenSpec.bike192(),
BikeKeyGenSpec.bike256() };
for (var spec : bikes) {
for (var m : MODES) {
out.add(Arguments.of("BIKE", spec, m, "BIKE:" + labelOf(spec) + " · " + m));
}
}
// 7) HQC
var hqcs = new HqcKeyGenSpec[] { HqcKeyGenSpec.hqc128(), HqcKeyGenSpec.hqc192(), HqcKeyGenSpec.hqc256() };
for (var spec : hqcs) {
for (var m : MODES) {
out.add(Arguments.of("HQC", spec, m, "HQC:" + labelOf(spec) + " · " + m));
}
}
// 8) SNTRU Prime
var sntrus = new SntruPrimeKeyGenSpec[] { SntruPrimeKeyGenSpec.sntrup653(), SntruPrimeKeyGenSpec.sntrup761(),
SntruPrimeKeyGenSpec.sntrup857(), SntruPrimeKeyGenSpec.sntrup953(), SntruPrimeKeyGenSpec.sntrup1013(),
SntruPrimeKeyGenSpec.sntrup1277() };
for (var spec : sntrus) {
for (var m : MODES) {
out.add(Arguments.of("SNTRUPrime", spec, m, "SNTRUPrime:" + labelOf(spec) + " · " + m));
}
}
// 9) NTRU LPRime
var ntrul = new NtrulPrimeKeyGenSpec[] { NtrulPrimeKeyGenSpec.ntrulpr653(), NtrulPrimeKeyGenSpec.ntrulpr761(),
NtrulPrimeKeyGenSpec.ntrulpr857(), NtrulPrimeKeyGenSpec.ntrulpr953(),
NtrulPrimeKeyGenSpec.ntrulpr1013(), NtrulPrimeKeyGenSpec.ntrulpr1277() };
for (var spec : ntrul) {
for (var m : MODES) {
out.add(Arguments.of("NTRULPRime", spec, m, "NTRULPRime:" + labelOf(spec) + " · " + m));
}
}
return out.stream();
}
@ParameterizedTest(name = "{3}")
@MethodSource("vectors")
@zeroecho.core.annotation.DisplayName("KEM→symmetric (AES/ChaCha, single-thread) round-trip — generic")
void kemRoundTripGeneric(String kemId, AlgorithmKeySpec keyGenSpec, String mode, String pretty) throws Exception {
System.out.println("kemRoundTrip(" + pretty + ")");
// produce a pseudo-random payload
final int size = (128 * 1024) + 7;
final byte[] input = new byte[size];
new Random(123456789L).nextBytes(input);
// keypair via generic registry path
KeyPair kp = CryptoAlgorithms.keyPair(kemId, keyGenSpec);
// encrypt
DataContent enc = encryptStage(kemId, kp, mode);
enc.setInput(new BytesContent(input));
final byte[] encrypted;
try (InputStream is = enc.getStream()) {
encrypted = is.readAllBytes();
}
// decrypt
DataContent dec = decryptStage(kemId, kp, mode);
dec.setInput(new BytesContent(encrypted));
final byte[] decrypted;
try (InputStream is = dec.getStream()) {
decrypted = is.readAllBytes();
}
System.out.println("... input size: " + input.length);
System.out.println("... encrypted size: " + encrypted.length);
System.out.println("... decrypted size: " + decrypted.length);
assertArrayEquals(input, decrypted, "round-trip mismatch");
System.out.println("...ok");
}
private static DataContent encryptStage(String kemId, KeyPair kp, String mode) {
KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId).recipientPublic(kp.getPublic())
.derivedKeyBytes(32); // AES-256 or ChaCha20 key
switch (mode) {
case "GCM": {
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeGcm(128).withHeader().withAad(AAD);
return kem.withAes(aes).build(true);
}
case "CBC": {
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
return kem.withAes(aes).build(true);
}
case "CTR": {
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCtr().withHeader();
return kem.withAes(aes).build(true);
}
case "CHACHA20-POLY1305": {
ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder().withAad(AAD) // non-empty → AEAD
// variant
.withHeader(); // carry nonce
return kem.withChaCha(ch).build(true);
}
default:
throw new IllegalArgumentException("Unknown mode: " + mode);
}
}
private static DataContent decryptStage(String kemId, KeyPair kp, String mode) {
KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId).recipientPrivate(kp.getPrivate())
.derivedKeyBytes(32);
switch (mode) {
case "GCM": {
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeGcm(128).withHeader().withAad(AAD);
return kem.withAes(aes).build(false);
}
case "CBC": {
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
return kem.withAes(aes).build(false);
}
case "CTR": {
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCtr().withHeader();
return kem.withAes(aes).build(false);
}
case "CHACHA20-POLY1305": {
ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder().withAad(AAD).withHeader();
return kem.withChaCha(ch).build(false);
}
default:
throw new IllegalArgumentException("Unknown mode: " + mode);
}
}
/** Simple byte-array source for the pipeline. */
private static final class BytesContent implements PlainContent {
private final byte[] data;
BytesContent(byte[] data) {
this.data = data;
}
@Override
public InputStream getStream() throws IOException {
return new ByteArrayInputStream(data);
}
}
// Sanity: HKDF provider is available (exercise earlier utility)
@org.junit.jupiter.api.Test
void hkdfAvailable() {
assertDoesNotThrow(() -> Kdf.hkdfSha256(new byte[] { 1, 2, 3 }, null, null, 32));
}
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* 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.sdk.content.builtin;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class SecretPasswordTest {
@Test
void testGetPlainText() {
int len = 12;
System.out.printf("generating password, %d characters...%n", len);
SecretPassword sp = new SecretPassword(len);
System.out.printf("...string %s (length %d)%n", sp.toText(), sp.toText().length());
assertEquals(len, sp.toText().length());
}
}

View File

@@ -0,0 +1,113 @@
/*******************************************************************************
* 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.sdk.content.export;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import org.junit.jupiter.api.Test;
class Base64StreamTest {
@Test
void testEncodedOutputFrom4KBInput() throws Exception {
System.out.println("testEncodedOutputFrom4KBInput");
byte[] inputBytes = new byte[4096];
new SecureRandom().nextBytes(inputBytes); // random 4KB data
byte[] inputEncoded = Base64.getEncoder().encode(inputBytes);
System.out.println("...encoded length should be: " + inputEncoded.length);
int lineLength = 76;
String[] prefixes = { "echo ", null };
byte[][] lineSeparators = { " >> file.tmp\r\n".getBytes(StandardCharsets.US_ASCII),
"\n".getBytes(StandardCharsets.US_ASCII), null };
for (String prefix : prefixes) {
for (byte[] lineSep : lineSeparators) {
System.out.printf("...***** testing with prefix=%s and lineSep=%s%n",
prefix == null ? "null" : "\"" + prefix + "\"",
lineSep == null ? "null" : "\"" + new String(lineSep, StandardCharsets.US_ASCII) + "\"");
try (InputStream base64Stream = new Base64Stream(new ByteArrayInputStream(inputBytes),
prefix == null ? null : prefix.getBytes(StandardCharsets.US_ASCII), lineLength, lineSep)) {
String encodedOutput = new String(base64Stream.readAllBytes(), StandardCharsets.US_ASCII);
System.out.println(encodedOutput);
String[] lines = encodedOutput.split("\n");
for (int i = 0; i < lines.length; i++) {
int space = lines[i].indexOf(' ');
if (space > 0) {
lines[i] = lines[i].split(" ")[prefix == null ? 0 : 1];
}
}
if (lineSep != null) {
for (String line : lines) {
assertTrue(line.length() <= lineLength, "Line exceeds max length: " + line.length());
}
}
String joined = String.join("", lines);
System.out.println("...lines: " + lines.length);
System.out.println("......encoded length: " + joined.length());
if (lineSep == null && prefix != null) {
continue;
}
byte[] decoded = Base64.getDecoder().decode(joined);
System.out.println("...decoded length: " + decoded.length);
assertArrayEquals(inputBytes, decoded, "Decoded content does not match original input");
}
}
}
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,738 @@
/*******************************************************************************
* 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.sdk.guard;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.util.function.Supplier;
import java.util.random.RandomGenerator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec;
import zeroecho.core.alg.elgamal.ElgamalParamSpec;
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
import zeroecho.core.alg.rsa.RsaKeyGenSpec;
import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec;
import zeroecho.core.context.EncryptionContext;
import zeroecho.core.context.KemContext;
import zeroecho.core.tag.TagEngineBuilder;
import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
import zeroecho.sdk.builders.core.DataContentBuilder;
import zeroecho.sdk.builders.core.DataContentChainBuilder;
import zeroecho.sdk.content.api.DataContent;
import zeroecho.sdk.content.api.PlainContent;
/**
* JDK21+ JUnit (no 'var') that prints sizes immediately after each stage: -
* Password guardian - Single RSA recipient - Multi-recipient (Password + RSA +
* ML-KEM) + optional ElGamal - AES-GCM and AES-CBC/PKCS7 Prints method name
* (with params if any) at start, "...ok" at the end.
*/
public class MultiRecipientEnvelopeTest {
@BeforeAll
static void addProviders() {
Security.addProvider(new BouncyCastlePQCProvider()); // ML-KEM
Security.addProvider(new BouncyCastleProvider()); // extras
}
// ------------------------------------------------------------------------------------
// Password guardian
// ------------------------------------------------------------------------------------
@Test
@DisplayName("Password guardian + AES-256-GCM")
void testPasswordGuardian_Aes256Gcm() throws Exception {
final String method = "testPasswordGuardian_Aes256Gcm()";
System.out.println(method);
final byte[] input = randomInput(128 * 1024 + 7);
System.out.println("... input size: " + input.length);
final char[] password = "CorrectHorseBatteryStaple".toCharArray();
// AES-256-GCM with header so IV/tag are persisted in-band
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
// Encrypt
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
.payloadKeyBytes(32)
.addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32);
DataContent encryptor = enc.build(true);
encryptor.setInput(new BytesContent(input));
byte[] encrypted;
try (InputStream es = encryptor.getStream()) {
encrypted = readAllBytesAndPrint(es, "... encrypted size");
}
// Decrypt
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password));
DataContent decryptor = dec.build(false);
decryptor.setInput(new BytesContent(encrypted));
byte[] decrypted;
try (InputStream ds = decryptor.getStream()) {
decrypted = readAllBytesAndPrint(ds, "... decrypted size");
}
assertArrayEquals(input, decrypted);
System.out.println("...ok");
}
@Test
@DisplayName("Password guardian + AES-256-CBC/PKCS7")
void testPasswordGuardian_Aes256Cbc() throws Exception {
final String method = "testPasswordGuardian_Aes256Cbc()";
System.out.println(method);
final byte[] input = randomInput(128 * 1024 + 7);
System.out.println("... input size: " + input.length);
final char[] password = "Tr0ub4dor&3".toCharArray();
// AES-256-CBC with PKCS7 padding, header persists IV
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
.payloadKeyBytes(32)
.addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32);
DataContent encryptor = enc.build(true);
encryptor.setInput(new BytesContent(input));
byte[] encrypted;
try (InputStream es = encryptor.getStream()) {
encrypted = readAllBytesAndPrint(es, "... encrypted size");
}
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password));
DataContent decryptor = dec.build(false);
decryptor.setInput(new BytesContent(encrypted));
byte[] decrypted;
try (InputStream ds = decryptor.getStream()) {
decrypted = readAllBytesAndPrint(ds, "... decrypted size");
}
assertArrayEquals(input, decrypted);
System.out.println("...ok");
}
// ------------------------------------------------------------------------------------
// Single ElGamal recipient (via EncryptionContext)
// ------------------------------------------------------------------------------------
@Test
@DisplayName("Single ElGamal(PKCS1 default) recipient + AES-256-GCM")
void testSingleElgamalGuardian_Aes256Gcm() throws Exception {
final String method = "testSingleElgamalGuardian_Aes256Gcm()";
System.out.println(method);
final byte[] input = randomInput(128 * 1024 + 7);
System.out.println("... input size: " + input.length);
KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
.payloadKeyBytes(32).addRecipient(elgEnc);
DataContent encryptor = enc.build(true);
encryptor.setInput(new BytesContent(input));
byte[] encrypted;
try (InputStream es = encryptor.getStream()) {
encrypted = readAllBytesAndPrint(es, "... encrypted size");
}
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(elg.getPrivate()));
DataContent decryptor = dec.build(false);
decryptor.setInput(new BytesContent(encrypted));
byte[] decrypted;
try (InputStream ds = decryptor.getStream()) {
decrypted = readAllBytesAndPrint(ds, "... decrypted size");
}
assertArrayEquals(input, decrypted);
System.out.println("...ok");
}
@Test
@DisplayName("Single ElGamal(PKCS1 default) recipient + AES-256-CBC")
void testSingleElgamalGuardian_Aes256Cbc() throws Exception {
final String method = "testSingleElgamalGuardian_Aes256Cbc()";
System.out.println(method);
final byte[] input = randomInput(128 * 1024 + 13); // cross blocks
System.out.println("... input size: " + input.length);
KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
.payloadKeyBytes(32).addRecipient(elgEnc);
DataContent encryptor = enc.build(true);
encryptor.setInput(new BytesContent(input));
byte[] encrypted;
try (InputStream es = encryptor.getStream()) {
encrypted = readAllBytesAndPrint(es, "... encrypted size");
}
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(elg.getPrivate()));
DataContent decryptor = dec.build(false);
decryptor.setInput(new BytesContent(encrypted));
byte[] pt;
try (InputStream ds = decryptor.getStream()) {
pt = readAllBytesAndPrint(ds, "... decrypted size");
}
assertArrayEquals(input, pt);
System.out.println("...ok");
}
// ------------------------------------------------------------------------------------
// Single RSA recipient (via EncryptionContext)
// ------------------------------------------------------------------------------------
@Test
@DisplayName("Single RSA-OAEP(default) recipient + AES-256-GCM")
void testSingleRsaGuardian_Aes256Gcm() throws Exception {
final String method = "testSingleRsaGuardian_Aes256Gcm()";
System.out.println(method);
final byte[] input = randomInput(128 * 1024 + 7);
System.out.println("... input size: " + input.length);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(3072, new SecureRandom());
KeyPair rsa = kpg.generateKeyPair();
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
.payloadKeyBytes(32).addRecipient(rsaEnc);
DataContent encryptor = enc.build(true);
encryptor.setInput(new BytesContent(input));
byte[] encrypted;
try (InputStream es = encryptor.getStream()) {
encrypted = readAllBytesAndPrint(es, "... encrypted size");
}
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate()));
DataContent decryptor = dec.build(false);
decryptor.setInput(new BytesContent(encrypted));
byte[] decrypted;
try (InputStream ds = decryptor.getStream()) {
decrypted = readAllBytesAndPrint(ds, "... decrypted size");
}
assertArrayEquals(input, decrypted);
System.out.println("...ok");
}
@Test
@DisplayName("Single RSA-OAEP(default) recipient + AES-256-CBC/PKCS7")
void testSingleRsaGuardian_Aes256Cbc() throws Exception {
final String method = "testSingleRsaGuardian_Aes256Cbc()";
System.out.println(method);
final byte[] input = randomInput(128 * 1024 + 7);
System.out.println("... input size: " + input.length);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(3072, new SecureRandom());
KeyPair rsa = kpg.generateKeyPair();
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
.payloadKeyBytes(32).addRecipient(rsaEnc);
DataContent encryptor = enc.build(true);
encryptor.setInput(new BytesContent(input));
byte[] encrypted;
try (InputStream es = encryptor.getStream()) {
encrypted = readAllBytesAndPrint(es, "... encrypted size");
}
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate()));
DataContent decryptor = dec.build(false);
decryptor.setInput(new BytesContent(encrypted));
byte[] decrypted;
try (InputStream ds = decryptor.getStream()) {
decrypted = readAllBytesAndPrint(ds, "... decrypted size");
}
assertArrayEquals(input, decrypted);
System.out.println("...ok");
}
// ------------------------------------------------------------------------------------
// Multi-recipient (Password + RSA + KEM/ML-KEM + ElGamal) via contexts
// ------------------------------------------------------------------------------------
@Test
@DisplayName("Multi recipients (PWD+RSA+ML-KEM kyber512+ElGamal) + AES-256-GCM")
void testMultiRecipients_Kyber512_Aes256Gcm_AllUnlocks() throws Exception {
final String method = "testMultiRecipients_Kyber512_Aes256Gcm_AllUnlocks()";
System.out.println(method);
final byte[] input = randomInput(128 * 1024 + 7);
System.out.println("... input size: " + input.length);
final char[] password = "group-secret".toCharArray();
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(3072, new SecureRandom());
KeyPair rsa = kpg.generateKeyPair();
KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber512());
KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic());
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
.payloadKeyBytes(32)
.addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32)
.addRecipient(rsaEnc).addRecipient(elgEnc).addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32);
DataContent encryptor = enc.build(true);
encryptor.setInput(new BytesContent(input));
byte[] encrypted;
try (InputStream es = encryptor.getStream()) {
encrypted = readAllBytesAndPrint(es, "... encrypted size");
}
// Each unlock prints its decrypted size immediately
decryptAndAssert("... decrypt via password", aesGcm, new UnlockMaterial.Password(password), input, encrypted);
decryptAndAssert("... decrypt via RSA", aesGcm, new UnlockMaterial.Private(rsa.getPrivate()), input, encrypted);
decryptAndAssert("... decrypt via ML-KEM", aesGcm, new UnlockMaterial.Private(kyber.getPrivate()), input,
encrypted);
decryptAndAssert("... decrypt via ElGamal", aesGcm, new UnlockMaterial.Private(elg.getPrivate()), input,
encrypted);
System.out.println("...ok");
}
@Test
@DisplayName("Multi recipients (PWD+RSA+ML-KEM kyber768+ElGamal) + AES-256-CBC/PKCS7")
void testMultiRecipients_Kyber768_Aes256Cbc_AllUnlocks() throws Exception {
final String method = "testMultiRecipients_Kyber768_Aes256Cbc_AllUnlocks()";
System.out.println(method);
final byte[] input = randomInput(128 * 1024 + 7);
System.out.println("... input size: " + input.length);
final char[] password = "another-group-secret".toCharArray();
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(3072, new SecureRandom());
KeyPair rsa = kpg.generateKeyPair();
rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768());
KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic());
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
.payloadKeyBytes(32)
.addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32)
.addRecipient(rsaEnc).addRecipient(elgEnc).addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32);
DataContent encryptor = enc.build(true);
encryptor.setInput(new BytesContent(input));
byte[] encrypted;
try (InputStream es = encryptor.getStream()) {
encrypted = readAllBytesAndPrint(es, "... encrypted size");
}
decryptAndAssert("... decrypt via password", aesCbc, new UnlockMaterial.Password(password), input, encrypted);
decryptAndAssert("... decrypt via RSA", aesCbc, new UnlockMaterial.Private(rsa.getPrivate()), input, encrypted);
decryptAndAssert("... decrypt via ML-KEM", aesCbc, new UnlockMaterial.Private(kyber.getPrivate()), input,
encrypted);
decryptAndAssert("... decrypt via ElGamal", aesCbc, new UnlockMaterial.Private(elg.getPrivate()), input,
encrypted);
System.out.println("...ok");
}
// ====================================================================================
// Signed payload inside multi-recipient AES/GCM envelope (Ed25519 & SPHINCS+)
// ====================================================================================
@Test
@DisplayName("Signed payload (Ed25519) → Multi-recipient envelope (RSA + ML-KEM + PWD + ElGamal) → AES-256-GCM")
void testSignedPayload_Ed25519_MultiRecipients_AesGcm() throws Exception {
final String method = "testSignedPayload_Ed25519_MultiRecipients_AesGcm()";
System.out.println(method);
final byte[] msg = randomInput(96 * 1024 + 13);
System.out.println("... input size: " + msg.length);
// Recipients
final char[] password = "signed-group-secret".toCharArray();
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(3072, new SecureRandom());
KeyPair rsa = kpg.generateKeyPair();
KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768());
KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
// Sender signature keys (Ed25519)
KeyPair ed = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
// AES-256-GCM payload builder
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
// Context recipients
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic());
// Envelope (encrypt)
MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
.payloadKeyBytes(32).addRecipient(rsaEnc).addRecipient(elgEnc)
.addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 16)
.addPasswordRecipient(password, /* iterations */ 100_000, /* saltLen */ 16, /* kekBytes */ 32);
// Tag trailer for SIGNING (Ed25519)
TagTrailerDataContentBuilder<Signature> signTrailer = new TagTrailerDataContentBuilder<>(
TagEngineBuilder.ed25519Sign(ed.getPrivate())).bufferSize(8192);
// Encrypt chain
DataContent encryptChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(signTrailer)
.add(envEnc).build();
byte[] ciphertext;
try (InputStream es = encryptChain.getStream()) {
ciphertext = readAllBytesAndPrint(es, "... ciphertext size");
}
// Verify after each unlock
TagTrailerDataContentBuilder<Signature> verifyTrailer;
// via Password
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic()))
.bufferSize(8192).throwOnMismatch();
DataContent decPwd = DataContentChainBuilder
.decrypt().add(BytesSourceBuilder.of(ciphertext)).add(new MultiRecipientDataSourceBuilder()
.withAes(aesGcm.get()).payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password)))
.add(verifyTrailer).build();
byte[] ptPwd;
try (InputStream ds = decPwd.getStream()) {
ptPwd = readAllBytesAndPrint(ds, "... decrypted via password");
}
assertArrayEquals(msg, ptPwd);
// via RSA
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic()))
.bufferSize(8192).throwOnMismatch();
DataContent decRsa = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
.unlockWith(new UnlockMaterial.Private(rsa.getPrivate())))
.add(verifyTrailer).build();
byte[] ptRsa;
try (InputStream ds = decRsa.getStream()) {
ptRsa = readAllBytesAndPrint(ds, "... decrypted via RSA");
}
assertArrayEquals(msg, ptRsa);
// via ElGamal
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic()))
.bufferSize(8192).throwOnMismatch();
DataContent decElgamal = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
.unlockWith(new UnlockMaterial.Private(elg.getPrivate())))
.add(verifyTrailer).build();
byte[] ptElgamal;
try (InputStream ds = decElgamal.getStream()) {
ptElgamal = readAllBytesAndPrint(ds, "... decrypted via ElGamal");
}
assertArrayEquals(msg, ptElgamal);
// via ML-KEM
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic()))
.bufferSize(8192).throwOnMismatch();
DataContent decKem = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
.unlockWith(new UnlockMaterial.Private(kyber.getPrivate())))
.add(verifyTrailer).build();
byte[] ptKem;
try (InputStream ds = decKem.getStream()) {
ptKem = readAllBytesAndPrint(ds, "... decrypted via ML-KEM");
}
assertArrayEquals(msg, ptKem);
System.out.println("...ok");
}
@Test
@DisplayName("Signed payload (SPHINCS+) → Multi-recipient envelope (RSA + ML-KEM + PWD + ElGamal) → AES-256-GCM")
void testSignedPayload_SphincsPlus_MultiRecipients_AesGcm() throws Exception {
final String method = "testSignedPayload_SphincsPlus_MultiRecipients_AesGcm()";
System.out.println(method);
final byte[] msg = randomInput(64 * 1024 + 19);
System.out.println("... input size: " + msg.length);
// Recipients
final char[] password = "signed-group-secret-2".toCharArray();
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(3072, new SecureRandom());
KeyPair rsa = kpg.generateKeyPair();
KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber512());
KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
// Sender signature keys (SPHINCS+, default/best)
KeyPair spx = CryptoAlgorithms.keyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec());
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
// Context recipients
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic());
// Envelope recipients
MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
.payloadKeyBytes(32).addRecipient(rsaEnc).addRecipient(elgEnc)
.addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 16)
.addPasswordRecipient(password, /* iterations */ 120_000, /* saltLen */ 16, /* kekBytes */ 32);
// Tag trailer for SIGNING (SPHINCS+)
TagTrailerDataContentBuilder<Signature> signTrailer = new TagTrailerDataContentBuilder<>(
TagEngineBuilder.sphincsPlusSign(spx.getPrivate())).bufferSize(8192);
// Encrypt chain
DataContent encryptChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(signTrailer)
.add(envEnc).build();
byte[] ciphertext;
try (InputStream es = encryptChain.getStream()) {
ciphertext = readAllBytesAndPrint(es, "... ciphertext size");
}
// Verify trailer with SPHINCS+ public key after each unlock
TagTrailerDataContentBuilder<Signature> verifyTrailer;
// via Password
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic()))
.bufferSize(8192).throwOnMismatch();
DataContent decPwd = DataContentChainBuilder
.decrypt().add(BytesSourceBuilder.of(ciphertext)).add(new MultiRecipientDataSourceBuilder()
.withAes(aesGcm.get()).payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password)))
.add(verifyTrailer).build();
byte[] ptPwd;
try (InputStream ds = decPwd.getStream()) {
ptPwd = readAllBytesAndPrint(ds, "... decrypted via password");
}
assertArrayEquals(msg, ptPwd);
// via RSA
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic()))
.bufferSize(8192).throwOnMismatch();
DataContent decRsa = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
.unlockWith(new UnlockMaterial.Private(rsa.getPrivate())))
.add(verifyTrailer).build();
byte[] ptRsa;
try (InputStream ds = decRsa.getStream()) {
ptRsa = readAllBytesAndPrint(ds, "... decrypted via RSA");
}
assertArrayEquals(msg, ptRsa);
// via ElGamal
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic()))
.bufferSize(8192).throwOnMismatch();
DataContent decElgamal = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
.unlockWith(new UnlockMaterial.Private(elg.getPrivate())))
.add(verifyTrailer).build();
byte[] ptElgamal;
try (InputStream ds = decElgamal.getStream()) {
ptElgamal = readAllBytesAndPrint(ds, "... decrypted via ElGamal");
}
assertArrayEquals(msg, ptElgamal);
// via ML-KEM
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic()))
.bufferSize(8192).throwOnMismatch();
DataContent decKem = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
.unlockWith(new UnlockMaterial.Private(kyber.getPrivate())))
.add(verifyTrailer).build();
byte[] ptKem;
try (InputStream ds = decKem.getStream()) {
ptKem = readAllBytesAndPrint(ds, "... decrypted via ML-KEM");
}
assertArrayEquals(msg, ptKem);
System.out.println("...ok");
}
// ------------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------------
/** Minimal source builder so we can compose pull-style chains. */
private static final class BytesSourceBuilder implements DataContentBuilder<PlainContent> {
private final byte[] data;
private BytesSourceBuilder(byte[] data) {
this.data = data.clone();
}
static BytesSourceBuilder of(byte[] data) {
return new BytesSourceBuilder(data);
}
@Override
public PlainContent build(boolean encrypt) {
return new PlainContent() {
@Override
public void setInput(DataContent input) {
/* source: no upstream */ }
@Override
public InputStream getStream() {
return new ByteArrayInputStream(data);
}
};
}
}
private static void decryptAndAssert(String banner, Supplier<AesDataContentBuilder> aesFactory,
UnlockMaterial material, byte[] original, byte[] encrypted) throws IOException {
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesFactory.get())
.payloadKeyBytes(32).unlockWith(material);
DataContent decryptor = dec.build(false);
decryptor.setInput(new BytesContent(encrypted));
byte[] decrypted;
try (InputStream ds = decryptor.getStream()) {
decrypted = readAllBytesAndPrint(ds, banner + " -> size");
}
assertArrayEquals(original, decrypted);
}
/** Reads all bytes and prints the size in a finally block. */
private static byte[] readAllBytesAndPrint(InputStream in, String label) throws IOException {
byte[] result = new byte[0];
try {
result = in.readAllBytes();
return result;
} finally {
System.out.println(label + ": " + result.length);
}
}
private static byte[] randomInput(int size) {
byte[] data = new byte[size];
RandomGenerator rng = RandomGenerator.of("L64X256MixRandom");
rng.nextBytes(data);
return data;
}
/** Minimal PlainContent over a byte[]. */
private static final class BytesContent implements PlainContent {
private final byte[] data;
BytesContent(byte[] data) {
this.data = data;
}
@Override
public InputStream getStream() throws IOException {
return new ByteArrayInputStream(data);
}
}
}

View File

@@ -0,0 +1,75 @@
/*******************************************************************************
* 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.sdk.integrations.covert;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class TextualCodecTest {
@Test
void testGenerate100CharacterText() {
System.out.println("testGenerate100CharacterText");
TextualCodec.Generator generator = TextualCodec.Generator.EN;
String result = generator.getText(100);
System.out.println("...generated text: " + result);
assertNotNull(result, "Generated text should not be null");
assertEquals(100, result.length(), "Generated text should have 100 characters");
// Ensure all characters are from the expected alphabet
for (char c : result.toCharArray()) {
assertTrue(TextualCodec.Generator.ENGLISH.containsKey(c),
"Generated character '" + c + "' is not part of the English frequency table");
}
// Optional: Analyze distribution for debugging
Map<Character, Integer> histogram = new HashMap<>();
for (char c : result.toCharArray()) {
histogram.merge(c, 1, Integer::sum);
}
System.out.println("...character distribution: " + histogram);
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,186 @@
/*******************************************************************************
* 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.sdk.integrations.covert.jpeg;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import javax.crypto.SecretKey;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import conflux.Ctx;
import conflux.CtxInterface;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.alg.aes.AesKeyGenSpec;
import zeroecho.core.alg.aes.AesSpec;
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
import zeroecho.sdk.builders.core.DataContentChainBuilder;
import zeroecho.sdk.builders.core.PlainBytesBuilder;
import zeroecho.sdk.content.api.DataContent;
class JpegExifIntegrationTest {
@TempDir
Path tempDir;
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
in.transferTo(out);
return out.toByteArray();
}
@Test
void testEncryptEmbedExtractDecrypt() throws Exception {
System.out.println("testEncryptEmbedExtractDecrypt");
String inputText = "Hello, this is a secret message to embed in JPEG.";
inputText = inputText + inputText;
inputText = inputText + inputText;
inputText = inputText + inputText;
final byte[] inputBytes = inputText.getBytes(StandardCharsets.UTF_8);
final byte[] aad = { 1, 2, 3 };
System.out.println("...inputBytes.length=" + inputBytes.length);
// AES encryption setup
/*
* CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); SecretKey key =
* aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.
* aes256()); AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key,
* spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" +
* System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware)
* enc).setContext(session);
*/
SecretKey key = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
.generateSecret(AesKeyGenSpec.aes256());
CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
byte[] encryptedBytes;
DataContent dccb = DataContentChainBuilder.encrypt()
// input
.add(PlainBytesBuilder.builder().bytes(inputBytes))
// encryption
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded())
// using general AES/GCM/128 without specified header
.spec(AesSpec.gcm128(null))
// but let the builder add the default header for storing AAD and IV
.withHeader().withAad(aad).context(session))
// and create the pipeline
.build();
try (InputStream in = dccb.getStream()) {
encryptedBytes = readAll(in);
}
System.out.println("...encryptedBytes.length=" + encryptedBytes.length);
System.out.println("...-> " + Arrays.toString(encryptedBytes));
// Prepare JPEG test image
Path jpegOriginal = getResourcePath("test.jpg");
Path jpegEmbedded = tempDir.resolve("stego.jpg");
// Embed payload
try (InputStream payloadInput = new ByteArrayInputStream(encryptedBytes);
OutputStream jpegOutput = Files.newOutputStream(jpegEmbedded)) {
JpegExifEmbedder embedder = new JpegExifEmbedder();
embedder.setSlots(Slot.defaults());
int embed_size = embedder.embed(jpegOriginal, payloadInput, jpegOutput);
System.out.println("...embeddedStream.length=" + embed_size);
}
// Extract payload
ByteArrayOutputStream extractedEncrypted = new ByteArrayOutputStream();
JpegExifEmbedder embedder = new JpegExifEmbedder();
embedder.setSlots(Slot.defaults());
embedder.extract(jpegEmbedded, extractedEncrypted);
byte[] extractedEncryptedBytes = extractedEncrypted.toByteArray();
System.out.println("...extractedEncryptedBytes.length=" + extractedEncryptedBytes.length);
System.out.println("...-> " + Arrays.toString(extractedEncryptedBytes));
// Decrypt
dccb = DataContentChainBuilder.decrypt()
// input
.add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes))
// encryption
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null))
// let us use the default header for AAD and IV
.withHeader().withAad(aad).context(session))
// and create the pipeline
.build();
byte[] pt1;
try (InputStream in = dccb.getStream()) {
pt1 = readAll(in);
}
/*
* AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT,
* key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx
* byte[] pt1 = readAll(dec1.attach(new
* ByteArrayInputStream(extractedEncryptedBytes))); dec1.close();
*/
String decrypted = new String(pt1, StandardCharsets.UTF_8);
assertEquals(inputText, decrypted);
System.out.println("...ok");
}
private Path getResourcePath(String resource) throws URISyntaxException {
URL url = getClass().getClassLoader().getResource(resource);
if (url == null) {
throw new IllegalArgumentException("Missing resource: " + resource);
}
return Paths.get(url.toURI());
}
}

View File

@@ -0,0 +1,89 @@
/*******************************************************************************
* 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.sdk.integrations.stegano;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import org.junit.jupiter.api.Test;
public class LSBSteganographyMethodTest {
@Test
void testEmbedAndExtract() throws Exception {
SteganographyMethod method = new LSBSteganographyMethod();
String message = "Hello from LSB!";
InputStream imageIn = getClass().getResourceAsStream("/test.jpg");
InputStream msgIn = new ByteArrayInputStream(message.getBytes());
// Embed
InputStream embedded = method.embed(imageIn, ImageFormat.PNG, msgIn);
// Extract
InputStream extracted = method.extract(embedded);
String result = new String(extracted.readAllBytes());
assertEquals(message, result);
}
@Test
void testMetadata() {
SteganographyMethod method = new LSBSteganographyMethod();
StegoMetadata meta = method.getMetadata();
assertEquals("LSB", meta.name());
assertTrue(meta.fullName().contains("Least Significant"));
}
@Test
void createSample() throws Exception {
// for verification purposes
SteganographyMethod method = new LSBSteganographyMethod();
String message = "Hello from LSB!";
InputStream imageIn = getClass().getResourceAsStream("/test.jpg");
InputStream msgIn = new ByteArrayInputStream(message.getBytes());
// Embed
InputStream embedded = method.embed(imageIn, ImageFormat.PNG, msgIn);
embedded.transferTo(Files.newOutputStream(Paths.get("/tmp/lsb-test.png"), StandardOpenOption.CREATE));
}
}

View File

@@ -0,0 +1,208 @@
/*******************************************************************************
* 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.sdk.util;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link Kdf#hkdfSha256(byte[], byte[], byte[], int)} using RFC
* 5869 test vectors.
*
* <h2>What is covered</h2>
* <ul>
* <li>RFC 5869 Appendix A.1, A.2, A.3 known-answer tests for HKDF-SHA-256 (OKM
* only).</li>
* <li>Behavior with {@code salt == null} vs. {@code salt.length == 0} (both
* treated equivalently).</li>
* <li>Input immutability (arguments are not modified).</li>
* <li>Argument validation (empty IKM, invalid output length).</li>
* </ul>
*/
public class KdfTest {
private static byte[] hex(String s) {
return HexFormat.of().parseHex(s.replaceAll("\\s+", ""));
}
@Test
@DisplayName("RFC5869 A.1 - Basic test case with SHA-256")
void rfc5869_caseA1() throws Exception {
byte[] ikm = hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); // 22 times 0x0b
byte[] salt = hex("000102030405060708090a0b0c");
byte[] info = hex("f0f1f2f3f4f5f6f7f8f9");
int L = 42;
byte[] okm = Kdf.hkdfSha256(ikm, salt, info, L);
byte[] expectedOkm = hex(
"3cb25f25faacd57a90434f64d0362f2a" + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + "34007208d5b887185865");
assertArrayEquals(expectedOkm, okm, "OKM must match RFC5869 A.1");
}
@Test
@DisplayName("RFC5869 A.2 - Test with longer inputs/outputs")
void rfc5869_caseA2() throws Exception {
byte[] ikm = hex("000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1f"
+ "202122232425262728292a2b2c2d2e2f" + "303132333435363738393a3b3c3d3e3f"
+ "404142434445464748494a4b4c4d4e4f"); // 0x00..0x4f (80 bytes)
byte[] salt = hex("606162636465666768696a6b6c6d6e6f" + "707172737475767778797a7b7c7d7e7f"
+ "808182838485868788898a8b8c8d8e8f" + "909192939495969798999a9b9c9d9e9f"
+ "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"); // 0x60..0xaf (80 bytes)
byte[] info = hex("b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
+ "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef"
+ "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); // 0xb0..0xff (80 bytes)
int L = 82;
byte[] okm = Kdf.hkdfSha256(ikm, salt, info, L);
byte[] expectedOkm = hex("b11e398dc80327a1c8e7f78c596a4934" + "4f012eda2d4efad8a050cc4c19afa97c"
+ "59045a99cac7827271cb41c65e590e09" + "da3275600c2f09b8367793a9aca3db71"
+ "cc30c58179ec3e87c14c01d5c1f3434f" + "1d87");
assertArrayEquals(expectedOkm, okm, "OKM must match RFC5869 A.2");
}
@Test
@DisplayName("RFC5869 A.3 - Test with zero salt and empty info")
void rfc5869_caseA3() throws Exception {
byte[] ikm = hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); // 22 times 0x0b
byte[] salt = new byte[0]; // empty salt
byte[] info = new byte[0]; // empty info
int L = 42;
byte[] okm = Kdf.hkdfSha256(ikm, salt, info, L);
byte[] expectedOkm = hex(
"8da4e775a563c18f715f802a063c5a31" + "b8a11f5c5ee1879ec3454e5f3c738d2d" + "9d201395faa4b61a96c8");
assertArrayEquals(expectedOkm, okm, "OKM must match RFC5869 A.3");
}
@Test
@DisplayName("salt == null behaves like empty salt")
void nullSaltEqualsEmptySalt() throws Exception {
byte[] ikm = "ikm".getBytes(StandardCharsets.US_ASCII);
byte[] info = "info".getBytes(StandardCharsets.US_ASCII);
byte[] a = Kdf.hkdfSha256(ikm, null, info, 32);
byte[] b = Kdf.hkdfSha256(ikm, new byte[0], info, 32);
assertArrayEquals(a, b, "null salt and empty salt must be equivalent");
}
@Test
@DisplayName("Inputs are not mutated")
void inputsAreNotMutated() throws Exception {
byte[] ikm = hex("00112233aa55");
byte[] salt = hex("a0a1a2a3");
byte[] info = hex("b0b1");
byte[] ikmCopy = ikm.clone();
byte[] saltCopy = salt.clone();
byte[] infoCopy = info.clone();
Kdf.hkdfSha256(ikm, salt, info, 16);
assertArrayEquals(ikmCopy, ikm, "IKM must not be modified");
assertArrayEquals(saltCopy, salt, "salt must not be modified");
assertArrayEquals(infoCopy, info, "info must not be modified");
}
@Test
@DisplayName("Validation: empty IKM not allowed")
void emptyIkmDisallowed() {
assertThrows(IllegalArgumentException.class, () -> Kdf.hkdfSha256(new byte[0], null, null, 16));
}
@Test
@DisplayName("Validation: output length must be within 1..(255*HashLen)")
void invalidLengthsDisallowed() {
byte[] ikm = hex("01");
assertThrows(IllegalArgumentException.class, () -> Kdf.hkdfSha256(ikm, null, null, 0));
assertThrows(IllegalArgumentException.class, () -> Kdf.hkdfSha256(ikm, null, null, 255 * 32 + 1));
}
@Test
@DisplayName("Small sanity: different info yields different outputs")
void differentInfoProducesDifferentOkm() throws Exception {
byte[] ikm = "ikm".getBytes(StandardCharsets.US_ASCII);
byte[] salt = "salt".getBytes(StandardCharsets.US_ASCII);
byte[] info1 = "ctx1".getBytes(StandardCharsets.US_ASCII);
byte[] info2 = "ctx2".getBytes(StandardCharsets.US_ASCII);
byte[] okm1 = Kdf.hkdfSha256(ikm, salt, info1, 32);
byte[] okm2 = Kdf.hkdfSha256(ikm, salt, info2, 32);
assertNotEquals(HexFormat.of().formatHex(okm1), HexFormat.of().formatHex(okm2),
"Distinct info should produce distinct OKM");
}
@Test
@DisplayName("Idempotence for same inputs")
void repeatability() throws Exception {
byte[] ikm = "same-ikm".getBytes(StandardCharsets.US_ASCII);
byte[] salt = "same-salt".getBytes(StandardCharsets.US_ASCII);
byte[] info = "same-info".getBytes(StandardCharsets.US_ASCII);
byte[] a = Kdf.hkdfSha256(ikm, salt, info, 48);
byte[] b = Kdf.hkdfSha256(ikm, salt, info, 48);
assertArrayEquals(a, b, "Same inputs must yield identical OKM");
}
@Test
@DisplayName("Produces exact requested length")
void exactLengthProduced() throws Exception {
byte[] ikm = "ikm".getBytes(StandardCharsets.US_ASCII);
for (int len : new int[] { 1, 16, 32, 33, 64, 100 }) {
byte[] okm = Kdf.hkdfSha256(ikm, null, null, len);
assertEquals(len, okm.length, "OKM length must equal requested length");
}
}
@Test
@DisplayName("Declared throws: GeneralSecurityException path is reachable")
void macAlgorithmAvailable() throws Exception {
// This test will fail only if HmacSHA256 is unavailable, which would throw
// GeneralSecurityException.
assertDoesNotThrow(() -> Kdf.hkdfSha256(new byte[] { 1, 2, 3 }, null, null, 32));
}
}

View File

@@ -0,0 +1,119 @@
/*******************************************************************************
* 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.sdk.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.junit.jupiter.api.Test;
/**
* A simple concrete subclass of OutputToInputStreamAdapter that increments each
* byte of input by 1.
*/
class IncrementingAdapter extends OutputToInputStreamAdapter {
public IncrementingAdapter(InputStream previousInput) {
super(previousInput);
}
/**
* Initializes transformationOut as an OutputStream that increments bytes by 1.
*/
public void initialize() {
this.transformationOut = new OutputStream() {
@Override
public void write(int b) {
baos.write((b + 1) & 0xFF);
}
};
}
}
@Deprecated
public class OutputToInputStreamAdapterTest {
private static final int BUF_SIZE = 8192;
private InputStream generateLongInputStream() {
String phrase = "Hello World! ";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while (baos.size() <= 3 * BUF_SIZE) {
try {
baos.write(phrase.getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return new ByteArrayInputStream(baos.toByteArray());
}
@Test
public void testIncrementingAdapter() throws IOException {
System.out.println("testIncrementingAdapter");
InputStream plainInput = generateLongInputStream();
IncrementingAdapter adapter = new IncrementingAdapter(plainInput);
adapter.initialize();
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read;
while ((read = adapter.read(buffer)) != -1) {
result.write(buffer, 0, read);
}
adapter.close();
byte[] originalBytes = generateLongInputStream().readAllBytes();
byte[] transformedBytes = result.toByteArray();
System.out.println("..." + transformedBytes.length + " bytes processed");
assertEquals(originalBytes.length, transformedBytes.length, "Lengths must be equal");
for (int i = 0; i < originalBytes.length; i++) {
int expected = ((originalBytes[i] & 0xFF) + 1) & 0xFF;
int actual = transformedBytes[i] & 0xFF;
assertEquals(expected, actual, "Byte at position " + i + " should be incremented by 1");
}
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,239 @@
/*******************************************************************************
* 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.sdk.util;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
import org.junit.jupiter.api.Test;
class Pack7LStreamWriterTest {
@Test
void testSingleWriteCompletePayload() throws IOException {
System.out.println("testSingleWriteCompletePayload");
byte[] payload = new byte[50];
new Random().nextBytes(payload);
byte[] prefix = encodePack7L(payload.length);
byte[] full = new byte[prefix.length + payload.length];
System.arraycopy(prefix, 0, full, 0, prefix.length);
System.arraycopy(payload, 0, full, prefix.length, payload.length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Pack7LStreamWriter writer = new Pack7LStreamWriter(out);
int consumed = writer.write(full);
assertEquals(full.length, consumed);
assertArrayEquals(payload, out.toByteArray());
assertTrue(writer.isComplete());
System.out.println("...ok");
}
@Test
void testPartialLengthPrefixThenPayload() throws IOException {
System.out.println("testPartialLengthPrefixThenPayload");
byte[] payload = new byte[123];
new Random().nextBytes(payload);
byte[] prefix = encodePack7L(payload.length);
byte[] all = new byte[prefix.length + payload.length];
System.arraycopy(prefix, 0, all, 0, prefix.length);
System.arraycopy(payload, 0, all, prefix.length, payload.length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Pack7LStreamWriter writer = new Pack7LStreamWriter(out);
int partialPrefixLen = 2;
int consumed1 = writer.write(all, 0, partialPrefixLen);
assertEquals(partialPrefixLen, consumed1);
assertFalse(writer.isComplete());
int consumed2 = writer.write(all, consumed1, all.length - consumed1);
assertEquals(all.length - consumed1, consumed2);
assertTrue(writer.isComplete());
assertArrayEquals(payload, out.toByteArray());
System.out.println("...ok");
}
@Test
void testExtraDataTruncated() throws IOException {
System.out.println("testExtraDataTruncated");
byte[] payload = new byte[100];
new Random().nextBytes(payload);
byte[] extra = new byte[20];
new Random().nextBytes(extra);
byte[] prefix = encodePack7L(payload.length);
byte[] full = new byte[prefix.length + payload.length + extra.length];
System.arraycopy(prefix, 0, full, 0, prefix.length);
System.arraycopy(payload, 0, full, prefix.length, payload.length);
System.arraycopy(extra, 0, full, prefix.length + payload.length, extra.length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Pack7LStreamWriter writer = new Pack7LStreamWriter(out);
int consumed = writer.write(full);
assertEquals(prefix.length + payload.length, consumed);
assertArrayEquals(payload, out.toByteArray());
assertTrue(writer.isComplete());
System.out.println("...ok");
}
@Test
void testChunkedPayload() throws IOException {
System.out.println("testChunkedPayload");
byte[] payload = new byte[2048];
new Random().nextBytes(payload);
byte[] prefix = encodePack7L(payload.length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Pack7LStreamWriter writer = new Pack7LStreamWriter(out);
writer.write(prefix);
int totalConsumed = 0;
for (int i = 0; i < payload.length; i += 512) {
int chunkSize = Math.min(512, payload.length - i);
int consumed = writer.write(payload, i, chunkSize);
totalConsumed += consumed;
}
assertEquals(payload.length, totalConsumed);
assertArrayEquals(payload, out.toByteArray());
assertTrue(writer.isComplete());
System.out.println("...ok");
}
@Test
void testChunkedPayloadCycle() throws IOException {
System.out.println("testChunkedPayloadCycle");
byte[] payload = new byte[2048];
new Random().nextBytes(payload);
byte[] prefix = encodePack7L(payload.length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Pack7LStreamWriter writer = new Pack7LStreamWriter(out);
int prefix_consumed;
// prefix is also controlled with "consumed" counter
prefix_consumed = writer.write(prefix, 0, 1);
assertEquals(1, prefix_consumed);
prefix_consumed = writer.write(prefix, 1, prefix.length - 1);
assertEquals(prefix.length - 1, prefix_consumed);
int totalConsumed = 0;
for (int i = 0; i < payload.length * 10; i += 512) {
int chunkSize = Math.min(512, payload.length - i);
int consumed = writer.write(payload, i, chunkSize);
totalConsumed += consumed;
if (consumed < chunkSize) {
// the full block was not accepted
break;
}
}
assertEquals(payload.length, totalConsumed);
assertArrayEquals(payload, out.toByteArray());
assertTrue(writer.isComplete());
System.out.println("...ok");
}
@Test
void testWriteReturnsPartialIfTooMuchProvided() throws IOException {
System.out.println("testWriteReturnsPartialIfTooMuchProvided");
int payloadLen = 64;
byte[] payload = new byte[payloadLen];
new Random().nextBytes(payload);
byte[] extra = new byte[10];
new Random().nextBytes(extra);
byte[] prefix = encodePack7L(payloadLen);
byte[] full = new byte[prefix.length + payloadLen + extra.length];
System.arraycopy(prefix, 0, full, 0, prefix.length);
System.arraycopy(payload, 0, full, prefix.length, payloadLen);
System.arraycopy(extra, 0, full, prefix.length + payloadLen, extra.length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Pack7LStreamWriter writer = new Pack7LStreamWriter(out);
int consumed = writer.write(full);
assertEquals(prefix.length + payloadLen, consumed);
assertArrayEquals(payload, out.toByteArray());
assertTrue(writer.isComplete());
System.out.println("...ok");
}
/**
* Helper method to encode a long using 7-bit packed encoding.
*/
private static byte[] encodePack7L(long val) {
byte[] buf = new byte[10];
int idx = buf.length;
while ((val & ~0x7fL) != 0) {
buf[--idx] = (byte) (val & 0x7f);
val >>>= 7;
}
buf[--idx] = (byte) val;
buf[buf.length - 1] |= 0x80;
return Arrays.copyOfRange(buf, idx, buf.length);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB