Initial commit (history reset)
This commit is contained in:
491
lib/src/test/java/zeroecho/core/CatalogContractTest.java
Normal file
491
lib/src/test/java/zeroecho/core/CatalogContractTest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
192
lib/src/test/java/zeroecho/core/alg/aes/AesLargeDataTest.java
Normal file
192
lib/src/test/java/zeroecho/core/alg/aes/AesLargeDataTest.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
181
lib/src/test/java/zeroecho/core/alg/hmac/HmacLargeDataTest.java
Normal file
181
lib/src/test/java/zeroecho/core/alg/hmac/HmacLargeDataTest.java
Normal 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();
|
||||
}
|
||||
}
|
||||
139
lib/src/test/java/zeroecho/core/alg/rsa/RsaLargeDataTest.java
Normal file
139
lib/src/test/java/zeroecho/core/alg/rsa/RsaLargeDataTest.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
167
lib/src/test/java/zeroecho/core/io/UtilTest.java
Normal file
167
lib/src/test/java/zeroecho/core/io/UtilTest.java
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
208
lib/src/test/java/zeroecho/sdk/util/KdfTest.java
Normal file
208
lib/src/test/java/zeroecho/sdk/util/KdfTest.java
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
239
lib/src/test/java/zeroecho/sdk/util/Pack7LStreamWriterTest.java
Normal file
239
lib/src/test/java/zeroecho/sdk/util/Pack7LStreamWriterTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
BIN
lib/src/test/resources/test.jpg
Normal file
BIN
lib/src/test/resources/test.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
Reference in New Issue
Block a user