refactor!: consolidate crypto architecture and security model
* make ZeroEchoSession the sole policy, audit, and runtime boundary * replace combined key builders with operation-specific SPI and typed metadata * remove obsolete pre-release compatibility APIs and global crypto operations * finalize JCA agreement contexts and replace inheritance with composition * harden secret lifecycle, key destruction, hybrid KEX, PBKDF2, and audit handling * standardize PairSeq I/O and introduce immutable validated value types * migrate app, ext, samples, and required pki integration points * expand correctness, security, concurrency, and malformed-input coverage BREAKING CHANGE: removes deprecated pre-release global configuration, legacy context factories, combined key-builder contracts, String-based password APIs, unchecked PairSeq writing, BlockGeometry public fields, and other compatibility facades.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
|
||||
/**
|
||||
* Verifies stable value semantics for capability metadata defaults.
|
||||
*/
|
||||
class CapabilityValueSemanticsTest {
|
||||
|
||||
@Test
|
||||
void semanticallyEqualCapabilitiesHaveEqualHashes() {
|
||||
System.out.println("semanticallyEqualCapabilitiesHaveEqualHashes");
|
||||
Capability first = capability(() -> new TestSpec("SHA-256"));
|
||||
Capability second = capability(() -> new TestSpec("SHA-256"));
|
||||
|
||||
assertEquals(first, second);
|
||||
assertEquals(first.hashCode(), second.hashCode());
|
||||
|
||||
System.out.println("...hash=" + first.hashCode());
|
||||
System.out.println("semanticallyEqualCapabilitiesHaveEqualHashes...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultSupplierIsResolvedOnceAndRepeatedAccessIsStable() {
|
||||
System.out.println("defaultSupplierIsResolvedOnceAndRepeatedAccessIsStable");
|
||||
AtomicInteger evaluations = new AtomicInteger();
|
||||
Capability capability = capability(() -> {
|
||||
evaluations.incrementAndGet();
|
||||
return new TestSpec("SHA-256");
|
||||
});
|
||||
|
||||
ContextSpec first = capability.defaultSpec();
|
||||
ContextSpec second = capability.defaultSpec();
|
||||
assertEquals(1, evaluations.get());
|
||||
assertSame(first, second);
|
||||
|
||||
System.out.println("...evaluations=" + evaluations.get());
|
||||
System.out.println("defaultSupplierIsResolvedOnceAndRepeatedAccessIsStable...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void semanticallyDifferentCapabilitiesAreNotEqual() {
|
||||
System.out.println("semanticallyDifferentCapabilitiesAreNotEqual");
|
||||
Capability first = capability(() -> new TestSpec("SHA-256"));
|
||||
Capability second = capability(() -> new TestSpec("SHA-512"));
|
||||
|
||||
assertNotEquals(first, second);
|
||||
|
||||
System.out.println("...firstDefault=" + first.defaultSpec());
|
||||
System.out.println("...secondDefault=" + second.defaultSpec());
|
||||
System.out.println("semanticallyDifferentCapabilitiesAreNotEqual...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullAndIncompatibleDefaultsAreRejectedAtConstruction() {
|
||||
System.out.println("nullAndIncompatibleDefaultsAreRejectedAtConstruction");
|
||||
assertThrows(NullPointerException.class, () -> capability(() -> null));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new Capability("DIGEST", AlgorithmFamily.DIGEST, KeyUsage.DIGEST, DigestContext.class,
|
||||
NullKey.class, TestSpec.class, new OtherSpec()));
|
||||
|
||||
System.out.println("...invalidDefaultsRejected=true");
|
||||
System.out.println("nullAndIncompatibleDefaultsAreRejectedAtConstruction...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void metadataMemoizationDoesNotChangeRuntimeSupplierLifecycle() throws IOException {
|
||||
System.out.println("metadataMemoizationDoesNotChangeRuntimeSupplierLifecycle");
|
||||
AtomicInteger evaluations = new AtomicInteger();
|
||||
List<TestSpec> runtimeSpecs = new ArrayList<>();
|
||||
TestAlgorithm algorithm = new TestAlgorithm(evaluations, runtimeSpecs);
|
||||
Capability capability = algorithm.listCapabilities().get(0);
|
||||
|
||||
ContextSpec metadataFirst = capability.defaultSpec();
|
||||
ContextSpec metadataSecond = capability.defaultSpec();
|
||||
algorithm.createContext(KeyUsage.DIGEST, NullKey.INSTANCE, null);
|
||||
algorithm.createContext(KeyUsage.DIGEST, NullKey.INSTANCE, null);
|
||||
|
||||
assertSame(metadataFirst, metadataSecond);
|
||||
assertEquals(3, evaluations.get());
|
||||
assertNotEquals(runtimeSpecs.get(0), runtimeSpecs.get(1));
|
||||
System.out.println("...evaluations=" + evaluations.get());
|
||||
System.out.println("...runtimeDefaults=" + runtimeSpecs.size());
|
||||
System.out.println("metadataMemoizationDoesNotChangeRuntimeSupplierLifecycle...ok");
|
||||
}
|
||||
|
||||
private static Capability capability(java.util.function.Supplier<? extends ContextSpec> defaultSpec) {
|
||||
return new Capability("DIGEST", AlgorithmFamily.DIGEST, KeyUsage.DIGEST, DigestContext.class, NullKey.class,
|
||||
TestSpec.class, defaultSpec.get());
|
||||
}
|
||||
|
||||
private record TestSpec(String name) implements ContextSpec {
|
||||
}
|
||||
|
||||
private static final class OtherSpec implements ContextSpec {
|
||||
}
|
||||
|
||||
private static final class TestAlgorithm extends AbstractCryptoAlgorithm {
|
||||
private TestAlgorithm(AtomicInteger evaluations, List<TestSpec> runtimeSpecs) {
|
||||
super("TEST", "Test", "test");
|
||||
capability(AlgorithmFamily.DIGEST, KeyUsage.DIGEST, DigestContext.class, NullKey.class, TestSpec.class,
|
||||
(key, spec) -> {
|
||||
runtimeSpecs.add(spec);
|
||||
return mock(DigestContext.class);
|
||||
},
|
||||
() -> new TestSpec(Integer.toString(evaluations.incrementAndGet())));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,11 +71,16 @@ 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.core.KeyOperation;
|
||||
import zeroecho.core.KeyOperationInfo;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.SymmetricKeyGenerator;
|
||||
import zeroecho.core.audit.AuditMode;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
public class CatalogContractTest {
|
||||
private static ZeroEchoSession session;
|
||||
|
||||
static Level effectiveLevel(Logger lg) {
|
||||
for (Logger x = lg; x != null; x = x.getParent()) {
|
||||
@@ -102,9 +107,9 @@ public class CatalogContractTest {
|
||||
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);
|
||||
session = new ZeroEchoSession().withAuditListener(JulAuditListenerStd.builder().logger(jul)
|
||||
.infoLevel(Level.INFO).warnLevel(Level.WARNING).progressLevel(Level.FINE)
|
||||
.includeStackTraces(true).build()).withAuditMode(AuditMode.WRAP);
|
||||
|
||||
dump("");
|
||||
dump("zeroecho.core.audit");
|
||||
@@ -167,21 +172,21 @@ public class CatalogContractTest {
|
||||
void genericRoundTrips() throws Exception {
|
||||
logBegin();
|
||||
byte[] msg = "roundtrip".getBytes();
|
||||
CryptoAlgorithms.setAuditMode(CryptoAlgorithms.AuditMode.WRAP);
|
||||
|
||||
for (String id : CryptoAlgorithms.available()) {
|
||||
CryptoAlgorithm alg = CryptoAlgorithms.require(id);
|
||||
boolean hasAsym = alg.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE);
|
||||
boolean hasSym = alg.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE);
|
||||
|
||||
// SIGN/VERIFY (asymmetric)
|
||||
if (alg.roles().contains(KeyUsage.SIGN) && alg.roles().contains(KeyUsage.VERIFY)
|
||||
&& !alg.asymmetricBuildersInfo().isEmpty()) {
|
||||
&& hasAsym) {
|
||||
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);
|
||||
@@ -191,7 +196,7 @@ public class CatalogContractTest {
|
||||
|
||||
// KEM
|
||||
if (alg.roles().contains(KeyUsage.ENCAPSULATE) && alg.roles().contains(KeyUsage.DECAPSULATE)
|
||||
&& !alg.asymmetricBuildersInfo().isEmpty()) {
|
||||
&& hasAsym) {
|
||||
tryKem(id, msg);
|
||||
System.out.println();
|
||||
}
|
||||
@@ -203,7 +208,7 @@ public class CatalogContractTest {
|
||||
}
|
||||
|
||||
// MAC (single role now; verification via setExpectedTag)
|
||||
if (alg.roles().contains(KeyUsage.MAC) && !alg.symmetricBuildersInfo().isEmpty()) {
|
||||
if (alg.roles().contains(KeyUsage.MAC) && hasSym) {
|
||||
tryMac(id, msg);
|
||||
System.out.println();
|
||||
}
|
||||
@@ -222,7 +227,7 @@ public class CatalogContractTest {
|
||||
}
|
||||
|
||||
// SIGN: produce [body][signature] and capture trailer
|
||||
SignatureContext signer = CryptoAlgorithms.create(id, KeyUsage.SIGN, kp.getPrivate(), null);
|
||||
SignatureContext signer = session.createContext(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) {
|
||||
@@ -240,7 +245,7 @@ public class CatalogContractTest {
|
||||
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);
|
||||
SignatureContext verifier = session.createContext(id, KeyUsage.VERIFY, kp.getPublic(), null);
|
||||
verifier.setExpectedTag(sig);
|
||||
try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
readAll(verIn);
|
||||
@@ -261,14 +266,14 @@ public class CatalogContractTest {
|
||||
if (sk != null) {
|
||||
conflux.CtxInterface session = conflux.Ctx.INSTANCE.getContext("encdec-" + System.nanoTime());
|
||||
|
||||
EncryptionContext enc = CryptoAlgorithms.create(id, KeyUsage.ENCRYPT, sk, null);
|
||||
EncryptionContext enc = CatalogContractTest.session.createContext(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);
|
||||
EncryptionContext dec = CatalogContractTest.session.createContext(id, KeyUsage.DECRYPT, sk, null);
|
||||
if (dec instanceof zeroecho.core.spi.ContextAware ca2) {
|
||||
ca2.setContext(session);
|
||||
}
|
||||
@@ -284,11 +289,11 @@ public class CatalogContractTest {
|
||||
if (hasAsym) {
|
||||
KeyPair kp = tryKeyPairWithDefaultSpec(alg);
|
||||
if (kp != null) {
|
||||
EncryptionContext enc = CryptoAlgorithms.create(id, KeyUsage.ENCRYPT, kp.getPublic(), null);
|
||||
EncryptionContext enc = session.createContext(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);
|
||||
EncryptionContext dec = session.createContext(id, KeyUsage.DECRYPT, kp.getPrivate(), null);
|
||||
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
|
||||
dec.close();
|
||||
|
||||
@@ -308,8 +313,8 @@ public class CatalogContractTest {
|
||||
return;
|
||||
}
|
||||
|
||||
KemContext pub = CryptoAlgorithms.create(id, KeyUsage.ENCAPSULATE, kp.getPublic(), null);
|
||||
KemContext prv = CryptoAlgorithms.create(id, KeyUsage.DECAPSULATE, kp.getPrivate(), null);
|
||||
KemContext pub = session.createContext(id, KeyUsage.ENCAPSULATE, kp.getPublic(), null);
|
||||
KemContext prv = session.createContext(id, KeyUsage.DECAPSULATE, kp.getPrivate(), null);
|
||||
|
||||
KemContext.KemResult res = pub.encapsulate();
|
||||
byte[] ss = prv.decapsulate(res.ciphertext());
|
||||
@@ -326,7 +331,7 @@ public class CatalogContractTest {
|
||||
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);
|
||||
DigestContext dctx = session.createContext(id, KeyUsage.DIGEST, NullKey.INSTANCE, null);
|
||||
final byte[][] digestHolder = new byte[1][];
|
||||
final int tagLen = dctx.tagLength();
|
||||
|
||||
@@ -360,7 +365,7 @@ public class CatalogContractTest {
|
||||
}
|
||||
|
||||
// Produce tag: [body][tag], capture trailer
|
||||
MacContext mac = CryptoAlgorithms.create(id, KeyUsage.MAC, sk, null);
|
||||
MacContext mac = session.createContext(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) {
|
||||
@@ -378,7 +383,7 @@ public class CatalogContractTest {
|
||||
assertTrue(tag.length > 0);
|
||||
|
||||
// Verify: provide expected tag and drain (throws on mismatch)
|
||||
MacContext ver = CryptoAlgorithms.create(id, KeyUsage.MAC, sk, null);
|
||||
MacContext ver = session.createContext(id, KeyUsage.MAC, sk, null);
|
||||
ver.setExpectedTag(tag);
|
||||
try (InputStream in = ver.wrap(new ByteArrayInputStream(msg))) {
|
||||
readAll(in);
|
||||
@@ -406,33 +411,32 @@ public class CatalogContractTest {
|
||||
private KeyPair tryKeyPairWithDefaultSpec(CryptoAlgorithm alg) {
|
||||
logBegin(alg.id());
|
||||
try {
|
||||
List<CryptoAlgorithm.AsymBuilderInfo> infos = alg.asymmetricBuildersInfo();
|
||||
List<KeyOperationInfo> infos = alg.keyOperations().stream()
|
||||
.filter(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE).toList();
|
||||
if (infos.isEmpty()) {
|
||||
System.out.println("no asymmetric builder info");
|
||||
logEnd();
|
||||
return null;
|
||||
}
|
||||
|
||||
for (CryptoAlgorithm.AsymBuilderInfo bi : infos) {
|
||||
if (bi.defaultKeySpec == null) {
|
||||
for (KeyOperationInfo bi : infos) {
|
||||
if (bi.defaultSpec() == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType;
|
||||
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
|
||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType();
|
||||
AlgorithmKeySpec spec = bi.defaultSpec();
|
||||
|
||||
AsymmetricKeyBuilder<AlgorithmKeySpec> builder = alg.asymmetricKeyBuilder(specType);
|
||||
AsymmetricKeyPairGenerator<AlgorithmKeySpec> builder = alg.asymmetricKeyPairGenerator(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: "
|
||||
System.out.println("builder " + bi.specType().getSimpleName() + " failed to generate keypair: "
|
||||
+ t.getClass().getSimpleName() + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -449,30 +453,29 @@ public class CatalogContractTest {
|
||||
private SecretKey tryGenerateSecretWithDefaultSpec(CryptoAlgorithm alg) {
|
||||
logBegin(alg.id());
|
||||
try {
|
||||
List<CryptoAlgorithm.SymBuilderInfo> infos = alg.symmetricBuildersInfo();
|
||||
List<KeyOperationInfo> infos = alg.keyOperations().stream()
|
||||
.filter(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE).toList();
|
||||
if (infos.isEmpty()) {
|
||||
System.out.println("no symmetric builder info");
|
||||
logEnd();
|
||||
return null;
|
||||
}
|
||||
|
||||
for (CryptoAlgorithm.SymBuilderInfo bi : infos) {
|
||||
if (bi.defaultKeySpec() == null) {
|
||||
for (KeyOperationInfo bi : infos) {
|
||||
if (bi.defaultSpec() == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType();
|
||||
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec();
|
||||
AlgorithmKeySpec spec = bi.defaultSpec();
|
||||
|
||||
SymmetricKeyBuilder<AlgorithmKeySpec> builder = alg.symmetricKeyBuilder(specType);
|
||||
SymmetricKeyGenerator<AlgorithmKeySpec> builder = alg.symmetricKeyGenerator(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());
|
||||
|
||||
@@ -34,20 +34,41 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.security.Key;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms.AuditMode;
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
import zeroecho.core.audit.AuditedContexts;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.CryptoContext;
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.context.KemContext.KemResult;
|
||||
import zeroecho.core.context.MacContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.core.tag.TagEngine;
|
||||
|
||||
/**
|
||||
* Verifies audited proxy wrapping for representative {@link CryptoContext}
|
||||
@@ -55,7 +76,7 @@ import zeroecho.core.context.DigestContext;
|
||||
*
|
||||
* <p>
|
||||
* These tests focus on the internal audit wrapping path used by
|
||||
* {@link CryptoAlgorithms#wrapForAudit(CryptoContext, zeroecho.core.audit.AuditListener, KeyUsage)}.
|
||||
* {@link AuditedContexts#wrap(CryptoContext, AuditListener, KeyUsage)}.
|
||||
* They verify that representative context types are wrapped as audited JDK
|
||||
* proxies and that the resulting wrapper preserves the expected basic
|
||||
* delegation behavior.
|
||||
@@ -63,18 +84,12 @@ import zeroecho.core.context.DigestContext;
|
||||
*/
|
||||
class CryptoAlgorithmsAuditWrapTest {
|
||||
|
||||
@AfterEach
|
||||
void restoreAuditConfiguration() {
|
||||
CryptoAlgorithms.setAuditListener(AuditListener.noop());
|
||||
CryptoAlgorithms.setAuditMode(AuditMode.OFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapForAuditDigestContextReturnsProxy() {
|
||||
System.out.println("wrapForAuditDigestContextReturnsProxy");
|
||||
|
||||
DigestContext context = mock(DigestContext.class);
|
||||
DigestContext wrapped = CryptoAlgorithms.wrapForAudit(context, AuditListener.noop(), KeyUsage.DIGEST);
|
||||
DigestContext wrapped = (DigestContext) AuditedContexts.wrap(context, AuditListener.noop(), KeyUsage.DIGEST);
|
||||
|
||||
System.out.println("...ctxClass=" + wrapped.getClass().getName());
|
||||
assertTrue(Proxy.isProxyClass(wrapped.getClass()), "Digest context should be wrapped as JDK proxy");
|
||||
@@ -87,7 +102,8 @@ class CryptoAlgorithmsAuditWrapTest {
|
||||
System.out.println("wrapForAuditAgreementContextReturnsProxy");
|
||||
|
||||
AgreementContext context = mock(AgreementContext.class);
|
||||
AgreementContext wrapped = CryptoAlgorithms.wrapForAudit(context, AuditListener.noop(), KeyUsage.AGREEMENT);
|
||||
AgreementContext wrapped = (AgreementContext) AuditedContexts.wrap(context, AuditListener.noop(),
|
||||
KeyUsage.AGREEMENT);
|
||||
|
||||
System.out.println("...ctxClass=" + wrapped.getClass().getName());
|
||||
assertTrue(Proxy.isProxyClass(wrapped.getClass()), "Agreement context should be wrapped as JDK proxy");
|
||||
@@ -100,7 +116,7 @@ class CryptoAlgorithmsAuditWrapTest {
|
||||
System.out.println("wrapForAuditDigestContextCloseDelegatesToWrappedContext");
|
||||
|
||||
DigestContext context = mock(DigestContext.class);
|
||||
DigestContext wrapped = CryptoAlgorithms.wrapForAudit(context, AuditListener.noop(), KeyUsage.DIGEST);
|
||||
DigestContext wrapped = (DigestContext) AuditedContexts.wrap(context, AuditListener.noop(), KeyUsage.DIGEST);
|
||||
|
||||
wrapped.close();
|
||||
|
||||
@@ -108,4 +124,182 @@ class CryptoAlgorithmsAuditWrapTest {
|
||||
System.out.println("...wrappedCloseDelegated=true");
|
||||
System.out.println("wrapForAuditDigestContextCloseDelegatesToWrappedContext...ok");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapAllFamilies() throws Exception {
|
||||
System.out.println("wrapAllFamilies");
|
||||
byte[] body = { 1, 2, 3 };
|
||||
|
||||
RecordingListener encryptionEvents = new RecordingListener();
|
||||
EncryptionContext encryption = mock(EncryptionContext.class);
|
||||
when(encryption.attach(any(InputStream.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
EncryptionContext auditedEncryption = wrap(encryption, encryptionEvents, KeyUsage.ENCRYPT);
|
||||
assertArrayEquals(body, auditedEncryption.attach(new ByteArrayInputStream(body)).readAllBytes());
|
||||
encryptionEvents.assertCreationBefore("progress");
|
||||
|
||||
assertTagFamily(mockTag(SignatureContext.class), KeyUsage.SIGN, body);
|
||||
assertTagFamily(mockTag(MacContext.class), KeyUsage.MAC, body);
|
||||
assertTagFamily(mockTag(DigestContext.class), KeyUsage.DIGEST, body);
|
||||
|
||||
RecordingListener agreementEvents = new RecordingListener();
|
||||
AgreementContext agreement = mock(AgreementContext.class);
|
||||
PublicKey peer = mock(PublicKey.class);
|
||||
when(agreement.deriveSecret()).thenReturn(new byte[] { 4, 5 });
|
||||
AgreementContext auditedAgreement = wrap(agreement, agreementEvents, KeyUsage.AGREEMENT);
|
||||
auditedAgreement.setPeerPublic(peer);
|
||||
assertArrayEquals(new byte[] { 4, 5 }, auditedAgreement.deriveSecret());
|
||||
agreementEvents.assertCreationBefore("peer");
|
||||
agreementEvents.assertBefore("peer", "derived");
|
||||
|
||||
RecordingListener messageEvents = new RecordingListener();
|
||||
MessageAgreementContext messageAgreement = mock(MessageAgreementContext.class);
|
||||
when(messageAgreement.getPeerMessage()).thenReturn(new byte[] { 6, 7, 8 });
|
||||
when(messageAgreement.deriveSecret()).thenReturn(new byte[] { 9 });
|
||||
MessageAgreementContext auditedMessage = wrap(messageAgreement, messageEvents, KeyUsage.AGREEMENT);
|
||||
auditedMessage.setPeerMessage(new byte[] { 6 });
|
||||
assertArrayEquals(new byte[] { 6, 7, 8 }, auditedMessage.getPeerMessage());
|
||||
assertArrayEquals(new byte[] { 9 }, auditedMessage.deriveSecret());
|
||||
messageEvents.assertCreationBefore("message-set");
|
||||
messageEvents.assertBefore("message-set", "message-get");
|
||||
messageEvents.assertBefore("message-get", "derived");
|
||||
|
||||
RecordingListener kemEvents = new RecordingListener();
|
||||
KemContext kem = mock(KemContext.class);
|
||||
KemResult result = new KemResult(new byte[] { 10, 11 }, new byte[] { 12 });
|
||||
when(kem.encapsulate()).thenReturn(result);
|
||||
when(kem.decapsulate(any(byte[].class))).thenReturn(new byte[] { 12 });
|
||||
KemContext auditedKem = wrap(kem, kemEvents, KeyUsage.ENCAPSULATE);
|
||||
assertSame(result, auditedKem.encapsulate());
|
||||
assertArrayEquals(new byte[] { 12 }, auditedKem.decapsulate(new byte[] { 10, 11 }));
|
||||
kemEvents.assertCreationBefore("encapsulated");
|
||||
kemEvents.assertBefore("encapsulated", "decapsulated");
|
||||
|
||||
System.out.println("...families=7");
|
||||
System.out.println("wrapAllFamilies...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapFailureAndIdempotency() throws Exception {
|
||||
System.out.println("wrapFailureAndIdempotency");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
IOException expected = new IOException("controlled read failure");
|
||||
EncryptionContext target = mock(EncryptionContext.class);
|
||||
when(target.attach(any(InputStream.class))).thenReturn(new InputStream() {
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
throw expected;
|
||||
}
|
||||
});
|
||||
|
||||
EncryptionContext wrapped = wrap(target, listener, KeyUsage.DECRYPT);
|
||||
int eventsAfterCreation = listener.events.size();
|
||||
assertSame(wrapped, AuditedContexts.wrap(wrapped, listener, KeyUsage.DECRYPT));
|
||||
assertEquals(eventsAfterCreation, listener.events.size());
|
||||
|
||||
IOException actual = assertThrows(IOException.class,
|
||||
() -> wrapped.attach(new ByteArrayInputStream(new byte[0])).readAllBytes());
|
||||
assertSame(expected, actual);
|
||||
assertEquals(1, listener.count("failure"));
|
||||
listener.assertCreationBefore("failure");
|
||||
|
||||
System.out.println("...failureEvents=" + listener.count("failure"));
|
||||
System.out.println("wrapFailureAndIdempotency...ok");
|
||||
}
|
||||
|
||||
private static <T extends CryptoContext> T wrap(T context, RecordingListener listener, KeyUsage usage) {
|
||||
@SuppressWarnings("unchecked")
|
||||
T wrapped = (T) AuditedContexts.wrap(context, listener, usage);
|
||||
assertTrue(Proxy.isProxyClass(wrapped.getClass()));
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
private static <T extends CryptoContext & TagEngine<?>> T mockTag(Class<T> type) throws IOException {
|
||||
T context = mock(type);
|
||||
when(context.tagLength()).thenReturn(2);
|
||||
when(context.wrap(any(InputStream.class))).thenAnswer(invocation -> {
|
||||
InputStream upstream = invocation.getArgument(0);
|
||||
return new java.io.SequenceInputStream(upstream, new ByteArrayInputStream(new byte[] { 0, 0 }));
|
||||
});
|
||||
return context;
|
||||
}
|
||||
|
||||
private static void assertTagFamily(CryptoContext context, KeyUsage usage, byte[] body) throws IOException {
|
||||
RecordingListener listener = new RecordingListener();
|
||||
CryptoContext wrapped = wrap(context, listener, usage);
|
||||
TagEngine<?> engine = (TagEngine<?>) wrapped;
|
||||
byte[] output = engine.wrap(new ByteArrayInputStream(body)).readAllBytes();
|
||||
|
||||
assertArrayEquals(new byte[] { 1, 2, 3, 0, 0 }, output);
|
||||
assertEquals(1, listener.count("tag"));
|
||||
listener.assertCreationBefore("tag");
|
||||
}
|
||||
|
||||
private static final class RecordingListener implements AuditListener {
|
||||
private final List<String> events = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role,
|
||||
String keyFingerprint, Map<String, Object> specMeta) {
|
||||
events.add("meta");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(String contextId, long bodyBytes, long trailerBytes) {
|
||||
events.add("progress");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagProduced(String contextId, int tagLength, String policy) {
|
||||
events.add("tag");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgreementPeerSet(String contextId, String peerFingerprint) {
|
||||
events.add("peer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgreementDerived(String contextId, int secretLength) {
|
||||
events.add("derived");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgreementPeerMessageSet(String contextId, int messageLength) {
|
||||
events.add("message-set");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgreementPeerMessageGet(String contextId, int messageLength) {
|
||||
events.add("message-get");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKemEncapsulated(int ciphertextLength, int sharedSecretLength) {
|
||||
events.add("encapsulated");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKemDecapsulated(int sharedSecretLength) {
|
||||
events.add("decapsulated");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String contextId, String stage, String operation, Throwable cause) {
|
||||
events.add("failure");
|
||||
}
|
||||
|
||||
private int count(String event) {
|
||||
return (int) events.stream().filter(event::equals).count();
|
||||
}
|
||||
|
||||
private void assertCreationBefore(String event) {
|
||||
assertEquals("meta", events.get(0));
|
||||
assertEquals(1, count("meta"));
|
||||
assertTrue(events.indexOf(event) > 0);
|
||||
}
|
||||
|
||||
private void assertBefore(String first, String second) {
|
||||
assertTrue(events.indexOf(first) < events.indexOf(second));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
205
lib/src/test/java/zeroecho/core/CryptoArchitectureTest.java
Normal file
205
lib/src/test/java/zeroecho/core/CryptoArchitectureTest.java
Normal file
@@ -0,0 +1,205 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.security.Key;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.audit.AuditMode;
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.policy.CryptoPolicy;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
|
||||
/**
|
||||
* Verifies authoritative registry ownership and explicitly scoped runtime state.
|
||||
*/
|
||||
class CryptoArchitectureTest {
|
||||
|
||||
@Test
|
||||
void authoritativeRegistry() {
|
||||
System.out.println("authoritativeRegistry");
|
||||
CryptoCatalog catalog = CryptoCatalog.load();
|
||||
|
||||
assertSame(CryptoAlgorithms.registry(), catalog.algorithms());
|
||||
assertEquals(CryptoAlgorithms.available(), catalog.algorithms().keySet());
|
||||
for (String id : CryptoAlgorithms.available()) {
|
||||
assertSame(CryptoAlgorithms.require(id), catalog.algorithms().get(id));
|
||||
}
|
||||
|
||||
System.out.println("...providerCount=" + CryptoAlgorithms.available().size());
|
||||
System.out.println("authoritativeRegistry...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitPolicyOrder() throws Exception {
|
||||
System.out.println("explicitPolicyOrder");
|
||||
List<String> events = new ArrayList<>();
|
||||
AuditListener listener = policyOrderListener(events);
|
||||
ZeroEchoSession allowed = new ZeroEchoSession().withAuditListener(listener).withPolicy(
|
||||
(id, role, key, spec) -> events.add("policy"));
|
||||
try (DigestContext context = allowed.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) {
|
||||
assertSame(CryptoAlgorithms.require("DIGEST"), context.algorithm());
|
||||
}
|
||||
assertEquals(List.of("policy", "audit"), events);
|
||||
|
||||
events.clear();
|
||||
IllegalArgumentException denial = new IllegalArgumentException("controlled denial");
|
||||
ZeroEchoSession denied = new ZeroEchoSession().withAuditListener(listener).withPolicy(
|
||||
(id, role, key, spec) -> {
|
||||
events.add("policy");
|
||||
throw denial;
|
||||
});
|
||||
assertSame(denial, assertThrows(IllegalArgumentException.class,
|
||||
() -> denied.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)));
|
||||
assertEquals(List.of("policy"), events);
|
||||
|
||||
events.clear();
|
||||
IllegalStateException failure = new IllegalStateException("controlled policy failure");
|
||||
ZeroEchoSession failing = new ZeroEchoSession().withAuditListener(listener).withPolicy(
|
||||
(id, role, key, spec) -> {
|
||||
events.add("policy");
|
||||
throw failure;
|
||||
});
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> failing.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)));
|
||||
assertEquals(List.of("policy"), events);
|
||||
|
||||
System.out.println("...allow=1...deny=1...failure=1");
|
||||
System.out.println("explicitPolicyOrder...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isolatedAuditConfiguration() throws Exception {
|
||||
System.out.println("isolatedAuditConfiguration");
|
||||
AtomicInteger firstEvents = new AtomicInteger();
|
||||
AtomicInteger secondEvents = new AtomicInteger();
|
||||
AuditListener firstListener = contextListener(firstEvents);
|
||||
AuditListener secondListener = contextListener(secondEvents);
|
||||
ZeroEchoSession wrapped = new ZeroEchoSession().withAuditListener(firstListener)
|
||||
.withAuditMode(AuditMode.WRAP);
|
||||
ZeroEchoSession direct = new ZeroEchoSession().withAuditListener(secondListener);
|
||||
|
||||
try (DigestContext wrappedContext = wrapped.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) {
|
||||
assertTrue(Proxy.isProxyClass(wrappedContext.getClass()));
|
||||
}
|
||||
int wrappedEvents = firstEvents.get();
|
||||
assertTrue(wrappedEvents > 0);
|
||||
assertEquals(0, secondEvents.get());
|
||||
try (DigestContext directContext = direct.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) {
|
||||
assertTrue(!Proxy.isProxyClass(directContext.getClass()));
|
||||
}
|
||||
assertEquals(wrappedEvents, firstEvents.get());
|
||||
assertEquals(1, secondEvents.get());
|
||||
assertEquals(AuditMode.WRAP, wrapped.auditMode());
|
||||
assertEquals(AuditMode.OFF, direct.auditMode());
|
||||
|
||||
System.out.println("...firstEvents=" + firstEvents.get());
|
||||
System.out.println("...secondEvents=" + secondEvents.get());
|
||||
System.out.println("isolatedAuditConfiguration...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void immutableSessionChanges() {
|
||||
System.out.println("immutableSessionChanges");
|
||||
ZeroEchoSession base = new ZeroEchoSession();
|
||||
ZeroEchoSession changed = base.withAuditMode(AuditMode.WRAP)
|
||||
.withAuditListener(contextListener(new AtomicInteger()));
|
||||
|
||||
assertNotSame(base, changed);
|
||||
assertEquals(AuditMode.OFF, base.auditMode());
|
||||
assertEquals(AuditMode.WRAP, changed.auditMode());
|
||||
assertNotSame(base.auditListener(), changed.auditListener());
|
||||
|
||||
System.out.println("...baseMode=" + base.auditMode());
|
||||
System.out.println("...changedMode=" + changed.auditMode());
|
||||
System.out.println("immutableSessionChanges...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentSessionReads() throws Exception {
|
||||
System.out.println("concurrentSessionReads");
|
||||
ZeroEchoSession session = new ZeroEchoSession().withAuditMode(AuditMode.MANUAL);
|
||||
int taskCount = 16;
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
List<Future<Boolean>> futures = new ArrayList<>();
|
||||
for (int index = 0; index < taskCount; index++) {
|
||||
futures.add(executor.submit(() -> {
|
||||
start.await();
|
||||
for (int iteration = 0; iteration < 500; iteration++) {
|
||||
if (session.auditMode() != AuditMode.MANUAL || session.available().isEmpty()
|
||||
|| session.require("DIGEST") != CryptoAlgorithms.require("DIGEST")) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
return Boolean.TRUE;
|
||||
}));
|
||||
}
|
||||
start.countDown();
|
||||
for (Future<Boolean> future : futures) {
|
||||
assertTrue(future.get().booleanValue());
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
System.out.println("...completedTasks=" + taskCount);
|
||||
System.out.println("concurrentSessionReads...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualModeSilent() throws Exception {
|
||||
System.out.println("manualModeSilent");
|
||||
AtomicInteger events = new AtomicInteger();
|
||||
ZeroEchoSession session = new ZeroEchoSession().withAuditListener(contextListener(events))
|
||||
.withAuditMode(AuditMode.MANUAL);
|
||||
|
||||
try (DigestContext context = session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) {
|
||||
assertTrue(!Proxy.isProxyClass(context.getClass()));
|
||||
}
|
||||
|
||||
assertEquals(0, events.get());
|
||||
System.out.println("...events=" + events.get());
|
||||
System.out.println("manualModeSilent...ok");
|
||||
}
|
||||
|
||||
private static AuditListener contextListener(AtomicInteger count) {
|
||||
return new AuditListener() {
|
||||
@Override
|
||||
public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role,
|
||||
String keyFingerprint, java.util.Map<String, Object> specMeta) {
|
||||
count.incrementAndGet();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static AuditListener policyOrderListener(List<String> events) {
|
||||
return new AuditListener() {
|
||||
@Override
|
||||
public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role,
|
||||
String keyFingerprint, java.util.Map<String, Object> specMeta) {
|
||||
events.add("audit");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
244
lib/src/test/java/zeroecho/core/SecretSpecLifecycleTest.java
Normal file
244
lib/src/test/java/zeroecho/core/SecretSpecLifecycleTest.java
Normal file
@@ -0,0 +1,244 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.alg.aes.AesKeyImportSpec;
|
||||
import zeroecho.core.alg.aes.AesAlgorithm;
|
||||
import zeroecho.core.alg.chacha.ChaChaKeyImportSpec;
|
||||
import zeroecho.core.alg.hmac.HmacKeyImportSpec;
|
||||
import zeroecho.core.alg.mldsa.MldsaPrivateKeySpec;
|
||||
import zeroecho.core.alg.rsa.RsaPrivateKeySpec;
|
||||
import zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec;
|
||||
import zeroecho.core.alg.sphincsplus.SphincsPlusPrivateKeySpec;
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
|
||||
class SecretSpecLifecycleTest {
|
||||
private static final List<SpecCase> SPECS = List.of(
|
||||
new SpecCase("zeroecho.core.alg.aes.AesKeyImportSpec", "key", 16, Factory.STATIC_RAW),
|
||||
new SpecCase("zeroecho.core.alg.bike.BikePrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.chacha.ChaChaKeyImportSpec", "key", 32, Factory.STATIC_RAW),
|
||||
new SpecCase("zeroecho.core.alg.cmce.CmcePrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.dh.DhPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.ed25519.Ed25519PrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.ed448.Ed448PrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.elgamal.ElgamalPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.frodo.FrodoPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.hmac.HmacKeyImportSpec", "key", 8, Factory.HMAC),
|
||||
new SpecCase("zeroecho.core.alg.hqc.HqcPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.kyber.KyberPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.mldsa.MldsaPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.ntru.NtruPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.ntruprime.NtrulPrimePrivateKeySpec", "pkcs8", 8,
|
||||
Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.ntruprime.SntruPrimePrivateKeySpec", "pkcs8", 8,
|
||||
Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.rsa.RsaPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.saber.SaberPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.sphincsplus.SphincsPlusPrivateKeySpec", "encoded", 8,
|
||||
Factory.CONSTRUCTOR),
|
||||
new SpecCase("zeroecho.core.alg.xdh.XdhPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR));
|
||||
|
||||
@Test
|
||||
void allSecretSpecificationsOwnAndDestroyTheirByteArrays() throws Exception {
|
||||
System.out.print("SecretSpec/lifecycle...");
|
||||
for (SpecCase specCase : SPECS) {
|
||||
byte[] input = sequence(specCase.length());
|
||||
byte[] expected = input.clone();
|
||||
Object spec = specCase.create(input);
|
||||
Arrays.fill(input, (byte) 0);
|
||||
|
||||
Method accessor = spec.getClass().getMethod(specCase.accessor());
|
||||
byte[] first = (byte[]) accessor.invoke(spec);
|
||||
assertArrayEquals(expected, first, specCase.className());
|
||||
first[0] ^= 0x7f;
|
||||
assertArrayEquals(expected, (byte[]) accessor.invoke(spec), specCase.className());
|
||||
|
||||
Method marshal = spec.getClass().getMethod("marshal", spec.getClass());
|
||||
marshal.invoke(null, spec);
|
||||
Destroyable destroyable = assertInstanceOf(Destroyable.class, spec);
|
||||
assertFalse(destroyable.isDestroyed());
|
||||
destroyable.destroy();
|
||||
destroyable.destroy();
|
||||
assertTrue(destroyable.isDestroyed());
|
||||
assertAllSecretFieldsZero(spec);
|
||||
|
||||
InvocationTargetException accessFailure = assertThrows(InvocationTargetException.class,
|
||||
() -> accessor.invoke(spec), specCase.className());
|
||||
assertInstanceOf(IllegalStateException.class, accessFailure.getCause());
|
||||
InvocationTargetException marshalFailure = assertThrows(InvocationTargetException.class,
|
||||
() -> marshal.invoke(null, spec), specCase.className());
|
||||
assertInstanceOf(IllegalStateException.class, marshalFailure.getCause());
|
||||
}
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesNullAndBoundaryKeyMaterial() {
|
||||
System.out.print("SecretSpec/boundaries...");
|
||||
assertThrows(NullPointerException.class, () -> AesKeyImportSpec.fromRaw(null));
|
||||
assertThrows(IllegalArgumentException.class, () -> AesKeyImportSpec.fromRaw(new byte[0]));
|
||||
assertThrows(NullPointerException.class, () -> new HmacKeyImportSpec("HmacSHA256", null));
|
||||
assertThrows(NullPointerException.class, () -> new RsaPrivateKeySpec(null));
|
||||
|
||||
HmacKeyImportSpec emptyHmac = new HmacKeyImportSpec("HmacSHA256", new byte[0]);
|
||||
RsaPrivateKeySpec emptyRsa = new RsaPrivateKeySpec(new byte[0]);
|
||||
assertArrayEquals(new byte[0], emptyHmac.key());
|
||||
assertArrayEquals(new byte[0], emptyRsa.encoded());
|
||||
emptyHmac.destroy();
|
||||
emptyRsa.destroy();
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unmarshalCleansUpWhenMalformedDataFollowsValidSecretMaterial() {
|
||||
System.out.println("unmarshalCleansUpWhenMalformedDataFollowsValidSecretMaterial");
|
||||
String aesKey = Base64.getEncoder().encodeToString(sequence(16));
|
||||
String chachaKey = Base64.getEncoder().encodeToString(sequence(32));
|
||||
String encodedPrivateKey = Base64.getEncoder().encodeToString(sequence(8));
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> AesKeyImportSpec.unmarshal(PairSeq.of("k.b64", aesKey, "k.b64", "%")));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> ChaChaKeyImportSpec.unmarshal(PairSeq.of("k.b64", chachaKey, "k.b64", "%")));
|
||||
assertThrows(IllegalArgumentException.class, () -> HmacKeyImportSpec.unmarshal(
|
||||
PairSeq.of("mac", "HmacSHA256", "k.b64", encodedPrivateKey, "k.b64", "%")));
|
||||
assertThrows(IllegalArgumentException.class, () -> MldsaPrivateKeySpec.unmarshal(
|
||||
PairSeq.of("pkcs8.b64", encodedPrivateKey, "pkcs8.b64", "%")));
|
||||
assertThrows(IllegalArgumentException.class, () -> SlhDsaPrivateKeySpec.unmarshal(
|
||||
PairSeq.of("pkcs8.b64", encodedPrivateKey, "pkcs8.b64", "%")));
|
||||
assertThrows(IllegalArgumentException.class, () -> SphincsPlusPrivateKeySpec.unmarshal(
|
||||
PairSeq.of("pkcs8.b64", encodedPrivateKey, "pkcs8.b64", "%")));
|
||||
System.out.println("...cases=6");
|
||||
System.out.println("unmarshalCleansUpWhenMalformedDataFollowsValidSecretMaterial...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessAndDestroyAreLinearizable() throws Exception {
|
||||
System.out.print("SecretSpec/concurrent...");
|
||||
byte[] expected = sequence(16);
|
||||
AesKeyImportSpec spec = AesKeyImportSpec.fromRaw(expected);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
for (int i = 0; i < 32; i++) {
|
||||
futures.add(executor.submit(() -> {
|
||||
start.await();
|
||||
try {
|
||||
assertArrayEquals(expected, spec.key());
|
||||
} catch (IllegalStateException destroyed) {
|
||||
assertTrue(spec.isDestroyed());
|
||||
}
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
futures.add(executor.submit(() -> {
|
||||
start.await();
|
||||
spec.destroy();
|
||||
return null;
|
||||
}));
|
||||
start.countDown();
|
||||
for (Future<?> future : futures) {
|
||||
future.get();
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
assertTrue(spec.isDestroyed());
|
||||
assertThrows(IllegalStateException.class, spec::key);
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void importersDoNotDestroyCallerOwnedSpecifications() throws Exception {
|
||||
System.out.print("SecretSpec/import...");
|
||||
byte[] aesBytes = sequence(16);
|
||||
AesKeyImportSpec aesSpec = AesKeyImportSpec.fromRaw(aesBytes);
|
||||
AesAlgorithm aes = new AesAlgorithm();
|
||||
SecretKey secretKey = aes.symmetricKeyImporter(AesKeyImportSpec.class).importSecret(aesSpec);
|
||||
assertArrayEquals(aesBytes, secretKey.getEncoded());
|
||||
assertFalse(aesSpec.isDestroyed());
|
||||
assertArrayEquals(aesBytes, aesSpec.key());
|
||||
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(2048);
|
||||
KeyPair pair = generator.generateKeyPair();
|
||||
RsaPrivateKeySpec rsaSpec = new RsaPrivateKeySpec(pair.getPrivate().getEncoded());
|
||||
PrivateKey imported = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().importPrivate("RSA", rsaSpec);
|
||||
assertArrayEquals(pair.getPrivate().getEncoded(), imported.getEncoded());
|
||||
assertFalse(rsaSpec.isDestroyed());
|
||||
assertArrayEquals(pair.getPrivate().getEncoded(), rsaSpec.encoded());
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
private static byte[] sequence(int length) {
|
||||
byte[] bytes = new byte[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
bytes[i] = (byte) (i + 1);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static void assertAllSecretFieldsZero(Object spec) throws IllegalAccessException {
|
||||
for (Field field : spec.getClass().getDeclaredFields()) {
|
||||
if (field.getType() == byte[].class) {
|
||||
field.setAccessible(true);
|
||||
byte[] bytes = (byte[]) field.get(spec);
|
||||
assertTrue(Arrays.equals(new byte[bytes.length], bytes), spec.getClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum Factory {
|
||||
CONSTRUCTOR,
|
||||
STATIC_RAW,
|
||||
HMAC
|
||||
}
|
||||
|
||||
private record SpecCase(String className, String accessor, int length, Factory factory) {
|
||||
private Object create(byte[] input) throws ReflectiveOperationException {
|
||||
Class<?> type = Class.forName(className);
|
||||
if (factory == Factory.STATIC_RAW) {
|
||||
Method method = type.getMethod("fromRaw", byte[].class);
|
||||
return method.invoke(null, (Object) input);
|
||||
}
|
||||
if (factory == Factory.HMAC) {
|
||||
Constructor<?> constructor = type.getConstructor(String.class, byte[].class);
|
||||
return constructor.newInstance("HmacSHA256", input);
|
||||
}
|
||||
Constructor<?> constructor = type.getConstructor(byte[].class);
|
||||
return constructor.newInstance((Object) input);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
lib/src/test/java/zeroecho/core/TargetArchitectureTest.java
Normal file
154
lib/src/test/java/zeroecho/core/TargetArchitectureTest.java
Normal file
@@ -0,0 +1,154 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
import zeroecho.core.audit.AuditMode;
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
|
||||
/**
|
||||
* Guards the consolidated pre-release architecture against legacy API drift.
|
||||
*/
|
||||
class TargetArchitectureTest {
|
||||
|
||||
@Test
|
||||
void exactRegisteredKeyOperationMatrix() {
|
||||
String name = start("exactRegisteredKeyOperationMatrix");
|
||||
Map<KeyOperation, Integer> counts = new EnumMap<>(KeyOperation.class);
|
||||
Set<String> registrations = new HashSet<>();
|
||||
|
||||
for (String algorithmId : CryptoAlgorithms.available()) {
|
||||
CryptoAlgorithm algorithm = CryptoAlgorithms.require(algorithmId);
|
||||
for (KeyOperationInfo info : algorithm.keyOperations()) {
|
||||
String registration = algorithmId + "|" + info.operation() + "|" + info.specType().getName();
|
||||
assertTrue(registrations.add(registration), registration);
|
||||
assertLookupSucceeds(algorithm, info);
|
||||
counts.merge(info.operation(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(20, counts.getOrDefault(KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE, 0));
|
||||
assertEquals(20, counts.getOrDefault(KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, 0));
|
||||
assertEquals(20, counts.getOrDefault(KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, 0));
|
||||
assertEquals(4, counts.getOrDefault(KeyOperation.SYMMETRIC_GENERATE, 0));
|
||||
assertEquals(4, counts.getOrDefault(KeyOperation.SYMMETRIC_IMPORT, 0));
|
||||
progress("registrations=" + registrations.size());
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void obsoleteArchitectureTypesAreAbsent() {
|
||||
String name = start("obsoleteArchitectureTypesAreAbsent");
|
||||
assertThrows(ClassNotFoundException.class,
|
||||
() -> Class.forName("zeroecho.core.spi.ContextConstructorKS"));
|
||||
assertThrows(ClassNotFoundException.class,
|
||||
() -> Class.forName("zeroecho.core.spi.SymmetricKeyBuilder"));
|
||||
assertThrows(ClassNotFoundException.class,
|
||||
() -> Class.forName("zeroecho.core.spi.AsymmetricKeyBuilder"));
|
||||
|
||||
for (Method method : CryptoAlgorithms.class.getDeclaredMethods()) {
|
||||
if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) {
|
||||
assertTrue(method.getName().equals("available") || method.getName().equals("require"),
|
||||
method.toString());
|
||||
}
|
||||
}
|
||||
for (Field field : CryptoAlgorithms.class.getDeclaredFields()) {
|
||||
assertTrue(Modifier.isFinal(field.getModifiers()), field.toString());
|
||||
}
|
||||
Set<String> removedConveniences = Set.of("generateSecret", "importSecret", "generateKeyPair",
|
||||
"importPublic", "importPrivate");
|
||||
for (Method method : CryptoAlgorithm.class.getDeclaredMethods()) {
|
||||
assertFalse(removedConveniences.contains(method.getName()), method.toString());
|
||||
}
|
||||
progress("registryStatics=readOnly");
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void agreementImplementationIsFinal() {
|
||||
String name = start("agreementImplementationIsFinal");
|
||||
assertTrue(Modifier.isFinal(GenericJcaAgreementContext.class.getModifiers()));
|
||||
progress("final=true");
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void auditListenerFailureCannotChangeOperationOutcome() throws Exception {
|
||||
String name = start("auditListenerFailureCannotChangeOperationOutcome");
|
||||
AuditListener failing = new AuditListener() {
|
||||
@Override
|
||||
public void onContextCreatedMeta(String contextId, String algorithmId, String provider,
|
||||
KeyUsage role, String keyFingerprint, Map<String, Object> metadata) {
|
||||
throw new AssertionError("controlled listener failure");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextClosed(String contextId, long bodyBytes, long trailerBytes,
|
||||
long durationMillis) {
|
||||
throw new AssertionError("controlled listener failure");
|
||||
}
|
||||
};
|
||||
|
||||
for (AuditMode mode : new AuditMode[] { AuditMode.OFF, AuditMode.WRAP }) {
|
||||
ZeroEchoSession session = new ZeroEchoSession().withAuditListener(failing).withAuditMode(mode);
|
||||
try (DigestContext context = assertDoesNotThrow(
|
||||
() -> session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE))) {
|
||||
assertFalse(context.algorithm().id().isBlank());
|
||||
}
|
||||
}
|
||||
progress("modes=OFF,WRAP");
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void assertLookupSucceeds(CryptoAlgorithm algorithm, KeyOperationInfo info) {
|
||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) info.specType();
|
||||
switch (info.operation()) {
|
||||
case ASYMMETRIC_KEY_PAIR_GENERATE ->
|
||||
assertDoesNotThrow(() -> algorithm.asymmetricKeyPairGenerator(specType));
|
||||
case ASYMMETRIC_PUBLIC_IMPORT ->
|
||||
assertDoesNotThrow(() -> algorithm.publicKeyImporter(specType));
|
||||
case ASYMMETRIC_PRIVATE_IMPORT ->
|
||||
assertDoesNotThrow(() -> algorithm.privateKeyImporter(specType));
|
||||
case SYMMETRIC_GENERATE ->
|
||||
assertDoesNotThrow(() -> algorithm.symmetricKeyGenerator(specType));
|
||||
case SYMMETRIC_IMPORT ->
|
||||
assertDoesNotThrow(() -> algorithm.symmetricKeyImporter(specType));
|
||||
}
|
||||
}
|
||||
|
||||
private static String start(String routine) {
|
||||
String label = routine.length() <= 30 ? routine : routine.substring(0, 27) + "...";
|
||||
System.out.println(label);
|
||||
return label;
|
||||
}
|
||||
|
||||
private static void progress(String detail) {
|
||||
System.out.println("..." + detail);
|
||||
}
|
||||
|
||||
private static void ok(String name) {
|
||||
System.out.println(name + "...ok");
|
||||
}
|
||||
}
|
||||
55
lib/src/test/java/zeroecho/core/WrongKeySecurityTest.java
Normal file
55
lib/src/test/java/zeroecho/core/WrongKeySecurityTest.java
Normal file
@@ -0,0 +1,55 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.audit.AuditMode;
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
import zeroecho.core.err.UnsupportedSpecException;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
|
||||
class WrongKeySecurityTest {
|
||||
|
||||
@Test
|
||||
void wrongKeyMatrix() {
|
||||
System.out.println("wrongKeyMatrix");
|
||||
AtomicInteger auditEvents = new AtomicInteger();
|
||||
ZeroEchoSession session = new ZeroEchoSession().withAuditMode(AuditMode.WRAP)
|
||||
.withAuditListener(new AuditListener() {
|
||||
@Override
|
||||
public void onContextCreatedMeta(String contextId, String algorithmId, String provider,
|
||||
KeyUsage role, String keyFingerprint, java.util.Map<String, Object> specMeta) {
|
||||
auditEvents.incrementAndGet();
|
||||
}
|
||||
});
|
||||
|
||||
assertWrongKey(session, "AES", KeyUsage.ENCRYPT);
|
||||
assertWrongKey(session, "ECDSA", KeyUsage.SIGN);
|
||||
assertWrongKey(session, "Xdh", KeyUsage.AGREEMENT);
|
||||
assertWrongKey(session, "ML-KEM", KeyUsage.ENCAPSULATE);
|
||||
assertEquals(0, auditEvents.get(), "rejected keys must not create or process contexts");
|
||||
|
||||
System.out.println("...families=4");
|
||||
System.out.println("wrongKeyMatrix...ok");
|
||||
}
|
||||
|
||||
private static void assertWrongKey(ZeroEchoSession session, String algorithm, KeyUsage role) {
|
||||
UnsupportedSpecException failure = assertThrows(UnsupportedSpecException.class,
|
||||
() -> session.createContext(algorithm, role, NullKey.INSTANCE));
|
||||
assertTrue(failure.getMessage().contains(algorithm));
|
||||
assertTrue(failure.getMessage().contains(role.name()));
|
||||
assertNull(failure.getCause());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.security.KeyPair;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import conflux.Ctx;
|
||||
import conflux.CtxInterface;
|
||||
import zeroecho.core.audit.AuditMode;
|
||||
import zeroecho.core.alg.aes.AesSpec;
|
||||
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||
import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec;
|
||||
import zeroecho.core.alg.hmac.HmacSpec;
|
||||
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
|
||||
import zeroecho.core.alg.xdh.XdhSpec;
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.CryptoContext;
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.context.MacContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.io.TailStrippingInputStream;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.spi.ContextAware;
|
||||
import zeroecho.core.tag.TagEngine;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
class ZeroEchoSessionWrapIntegrationTest {
|
||||
private static final AtomicInteger CONTEXT_IDS = new AtomicInteger();
|
||||
|
||||
@BeforeAll
|
||||
static void initializeProvider() {
|
||||
BouncyCastleActivator.init();
|
||||
}
|
||||
|
||||
@Test
|
||||
void registeredStreamContexts() throws Exception {
|
||||
System.out.println("registeredStreamContexts");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
ZeroEchoSession session = wrappedSession(listener);
|
||||
byte[] message = { 1, 2, 3, 4, 5 };
|
||||
|
||||
SecretKey aesKey = new SecretKeySpec(new byte[16], "AES");
|
||||
CtxInterface aesContext = Ctx.INSTANCE.getContext("wrap-integration-" + CONTEXT_IDS.incrementAndGet());
|
||||
byte[] ciphertext;
|
||||
try (EncryptionContext encryption = session.createContext(
|
||||
"AES", KeyUsage.ENCRYPT, aesKey, AesSpec.gcm128(null))) {
|
||||
assertProxy(encryption);
|
||||
((ContextAware) encryption).setContext(aesContext);
|
||||
try (InputStream input = encryption.attach(new ByteArrayInputStream(message))) {
|
||||
ciphertext = input.readAllBytes();
|
||||
}
|
||||
}
|
||||
try (EncryptionContext decryption = session.createContext(
|
||||
"AES", KeyUsage.DECRYPT, aesKey, AesSpec.gcm128(null))) {
|
||||
assertProxy(decryption);
|
||||
((ContextAware) decryption).setContext(aesContext);
|
||||
try (InputStream input = decryption.attach(new ByteArrayInputStream(ciphertext))) {
|
||||
assertArrayEquals(message, input.readAllBytes());
|
||||
}
|
||||
}
|
||||
|
||||
KeyPair signingKeys = session.keyBuilders().asymmetric()
|
||||
.generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
|
||||
TaggedBody signature;
|
||||
try (SignatureContext signer = session.createContext(
|
||||
"Ed25519", KeyUsage.SIGN, signingKeys.getPrivate())) {
|
||||
assertProxy(signer);
|
||||
signature = produceTag(signer, message);
|
||||
}
|
||||
try (SignatureContext verifier = session.createContext(
|
||||
"Ed25519", KeyUsage.VERIFY, signingKeys.getPublic())) {
|
||||
assertProxy(verifier);
|
||||
verifier.setExpectedTag(signature.tag());
|
||||
try (InputStream input = verifier.wrap(new ByteArrayInputStream(message))) {
|
||||
assertArrayEquals(message, input.readAllBytes());
|
||||
}
|
||||
}
|
||||
|
||||
SecretKey hmacKey = new SecretKeySpec(new byte[32], "HmacSHA256");
|
||||
try (MacContext mac = session.createContext("HMAC", KeyUsage.MAC, hmacKey, HmacSpec.sha256())) {
|
||||
assertProxy(mac);
|
||||
assertArrayEquals(message, produceTag(mac, message).body());
|
||||
}
|
||||
try (DigestContext digest = session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) {
|
||||
assertProxy(digest);
|
||||
assertArrayEquals(message, produceTag(digest, message).body());
|
||||
}
|
||||
|
||||
assertEquals(List.of(
|
||||
"create:ENCRYPT", "create:DECRYPT",
|
||||
"create:SIGN", "tag", "create:VERIFY", "verify:true",
|
||||
"create:MAC", "tag", "create:DIGEST", "tag"), listener.events);
|
||||
System.out.println("...contexts=6...events=" + listener.events.size());
|
||||
System.out.println("registeredStreamContexts...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void registeredAgreementContexts() throws Exception {
|
||||
System.out.println("registeredAgreementContexts");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
ZeroEchoSession session = wrappedSession(listener);
|
||||
KeyPair aliceKeys = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobKeys = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
try (AgreementContext alice = session.createContext(
|
||||
"Xdh", KeyUsage.AGREEMENT, aliceKeys.getPrivate(), XdhSpec.X25519);
|
||||
AgreementContext bob = session.createContext(
|
||||
"Xdh", KeyUsage.AGREEMENT, bobKeys.getPrivate(), XdhSpec.X25519)) {
|
||||
assertProxy(alice);
|
||||
assertProxy(bob);
|
||||
alice.setPeerPublic(bobKeys.getPublic());
|
||||
bob.setPeerPublic(aliceKeys.getPublic());
|
||||
assertArrayEquals(alice.deriveSecret(), bob.deriveSecret());
|
||||
}
|
||||
|
||||
try (MessageAgreementContext alice = session.createContext(
|
||||
"Xdh", KeyUsage.AGREEMENT, new KeyPairKey(aliceKeys), XdhSpec.X25519);
|
||||
MessageAgreementContext bob = session.createContext(
|
||||
"Xdh", KeyUsage.AGREEMENT, new KeyPairKey(bobKeys), XdhSpec.X25519)) {
|
||||
assertProxy(alice);
|
||||
assertProxy(bob);
|
||||
byte[] aliceMessage = alice.getPeerMessage();
|
||||
byte[] bobMessage = bob.getPeerMessage();
|
||||
alice.setPeerMessage(bobMessage);
|
||||
bob.setPeerMessage(aliceMessage);
|
||||
assertArrayEquals(alice.deriveSecret(), bob.deriveSecret());
|
||||
}
|
||||
|
||||
assertEquals(List.of(
|
||||
"create:AGREEMENT", "create:AGREEMENT", "peer", "peer", "derived", "derived",
|
||||
"create:AGREEMENT", "create:AGREEMENT", "message-get", "message-get",
|
||||
"message-set", "message-set", "derived", "derived"), listener.events);
|
||||
System.out.println("...contexts=4...events=" + listener.events.size());
|
||||
System.out.println("registeredAgreementContexts...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void registeredKemContexts() throws Exception {
|
||||
System.out.println("registeredKemContexts");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
ZeroEchoSession session = wrappedSession(listener);
|
||||
KeyPair recipient = session.keyBuilders().asymmetric()
|
||||
.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512());
|
||||
|
||||
KemContext.KemResult encapsulated;
|
||||
try (KemContext encapsulator = session.createContext(
|
||||
"ML-KEM", KeyUsage.ENCAPSULATE, recipient.getPublic(), VoidSpec.INSTANCE);
|
||||
KemContext decapsulator = session.createContext(
|
||||
"ML-KEM", KeyUsage.DECAPSULATE, recipient.getPrivate(), VoidSpec.INSTANCE)) {
|
||||
assertProxy(encapsulator);
|
||||
assertProxy(decapsulator);
|
||||
encapsulated = encapsulator.encapsulate();
|
||||
assertArrayEquals(encapsulated.sharedSecret(), decapsulator.decapsulate(encapsulated.ciphertext()));
|
||||
}
|
||||
|
||||
try (MessageAgreementContext initiator = session.createContext(
|
||||
"ML-KEM", KeyUsage.AGREEMENT, recipient.getPublic(), VoidSpec.INSTANCE);
|
||||
MessageAgreementContext responder = session.createContext(
|
||||
"ML-KEM", KeyUsage.AGREEMENT, recipient.getPrivate(), VoidSpec.INSTANCE)) {
|
||||
assertProxy(initiator);
|
||||
assertProxy(responder);
|
||||
responder.setPeerMessage(initiator.getPeerMessage());
|
||||
assertArrayEquals(initiator.deriveSecret(), responder.deriveSecret());
|
||||
}
|
||||
|
||||
assertEquals(List.of(
|
||||
"create:ENCAPSULATE", "create:DECAPSULATE", "encapsulated", "decapsulated",
|
||||
"create:AGREEMENT", "create:AGREEMENT", "message-get", "message-set", "derived", "derived"),
|
||||
listener.events);
|
||||
System.out.println("...contexts=4...events=" + listener.events.size());
|
||||
System.out.println("registeredKemContexts...ok");
|
||||
}
|
||||
|
||||
private static ZeroEchoSession wrappedSession(RecordingListener listener) {
|
||||
return new ZeroEchoSession().withAuditListener(listener).withAuditMode(AuditMode.WRAP);
|
||||
}
|
||||
|
||||
private static TaggedBody produceTag(TagEngine<?> engine, byte[] body) throws IOException {
|
||||
byte[][] tag = new byte[1][];
|
||||
int tagLength = engine.tagLength();
|
||||
byte[] emittedBody;
|
||||
try (InputStream input = new TailStrippingInputStream(
|
||||
engine.wrap(new ByteArrayInputStream(body)), tagLength, 128) {
|
||||
@Override
|
||||
protected void processTail(byte[] tail) {
|
||||
tag[0] = tail.clone();
|
||||
}
|
||||
}) {
|
||||
emittedBody = input.readAllBytes();
|
||||
}
|
||||
return new TaggedBody(emittedBody, tag[0]);
|
||||
}
|
||||
|
||||
private static void assertProxy(CryptoContext context) {
|
||||
assertTrue(Proxy.isProxyClass(context.getClass()));
|
||||
}
|
||||
|
||||
private record TaggedBody(byte[] body, byte[] tag) {}
|
||||
|
||||
private static final class RecordingListener implements AuditListener {
|
||||
private final List<String> events = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role,
|
||||
String keyFingerprint, Map<String, Object> specMeta) {
|
||||
events.add("create:" + role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagProduced(String contextId, int tagLength, String policy) {
|
||||
events.add("tag");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVerifyResult(String contextId, boolean success, String policy, String expectedSource,
|
||||
int tagLength) {
|
||||
events.add("verify:" + success);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgreementPeerSet(String contextId, String peerFingerprint) {
|
||||
events.add("peer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgreementDerived(String contextId, int secretLength) {
|
||||
events.add("derived");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgreementPeerMessageSet(String contextId, int messageLength) {
|
||||
events.add("message-set");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgreementPeerMessageGet(String contextId, int messageLength) {
|
||||
events.add("message-get");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKemEncapsulated(int ciphertextLength, int sharedSecretLength) {
|
||||
events.add("encapsulated");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKemDecapsulated(int sharedSecretLength) {
|
||||
events.add("decapsulated");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.aes;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
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.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import conflux.Ctx;
|
||||
import conflux.CtxInterface;
|
||||
import zeroecho.core.ConfluxKeys;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.spi.ContextAware;
|
||||
|
||||
class AesDecryptionSecurityTest {
|
||||
private static final SecretKey KEY = new SecretKeySpec(repeated((byte) 0x11, 16), "AES");
|
||||
private static final SecretKey WRONG_KEY = new SecretKeySpec(repeated((byte) 0x22, 16), "AES");
|
||||
private static final AtomicInteger CONTEXT_IDS = new AtomicInteger();
|
||||
|
||||
@Test
|
||||
void gcmTamperMatrix() throws Exception {
|
||||
System.out.println("gcmTamperMatrix");
|
||||
byte[] plaintext = repeated((byte) 0x5a, 96);
|
||||
Encrypted encrypted = encrypt(AesSpec.gcm128(null), plaintext);
|
||||
|
||||
FailureResult modifiedBody = failedDecryption(
|
||||
encrypted.spec, KEY, encrypted.iv, changed(encrypted.ciphertext, 0));
|
||||
FailureResult modifiedTag = failedDecryption(encrypted.spec, KEY, encrypted.iv,
|
||||
changed(encrypted.ciphertext, encrypted.ciphertext.length - 1));
|
||||
FailureResult truncatedBody = failedDecryption(encrypted.spec, KEY, encrypted.iv,
|
||||
removed(encrypted.ciphertext, encrypted.ciphertext.length - 17));
|
||||
FailureResult truncatedTag = failedDecryption(encrypted.spec, KEY, encrypted.iv,
|
||||
Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length - 1));
|
||||
FailureResult trailing = failedDecryption(encrypted.spec, KEY, encrypted.iv,
|
||||
Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length + 1));
|
||||
FailureResult wrongKey = failedDecryption(encrypted.spec, WRONG_KEY, encrypted.iv, encrypted.ciphertext);
|
||||
FailureResult wrongIv = failedDecryption(
|
||||
encrypted.spec, KEY, changed(encrypted.iv, 0), encrypted.ciphertext);
|
||||
FailureResult empty = failedDecryption(encrypted.spec, KEY, encrypted.iv, new byte[0]);
|
||||
|
||||
for (FailureResult result : List.of(
|
||||
modifiedBody, modifiedTag, truncatedBody, truncatedTag, trailing, wrongKey, wrongIv, empty)) {
|
||||
assertEquals(0, result.outputBytes());
|
||||
assertAuthenticationFailure(result.failure());
|
||||
}
|
||||
assertThrows(IOException.class, () -> decrypt(encrypted.spec, KEY,
|
||||
Arrays.copyOf(encrypted.iv, encrypted.iv.length - 1), encrypted.ciphertext));
|
||||
assertThrows(IOException.class,
|
||||
() -> decrypt(encrypted.spec, KEY, null, encrypted.ciphertext));
|
||||
System.out.println("...cases=10");
|
||||
System.out.println("gcmTamperMatrix...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ctrTamperMatrix() throws Exception {
|
||||
System.out.println("ctrTamperMatrix");
|
||||
byte[] plaintext = repeated((byte) 0x33, 64);
|
||||
Encrypted encrypted = encrypt(AesSpec.ctr(null), plaintext);
|
||||
|
||||
byte[] wrongIv = decrypt(encrypted.spec, KEY, changed(encrypted.iv, 0), encrypted.ciphertext);
|
||||
byte[] wrongKey = decrypt(encrypted.spec, WRONG_KEY, encrypted.iv, encrypted.ciphertext);
|
||||
byte[] modified = decrypt(encrypted.spec, KEY, encrypted.iv, changed(encrypted.ciphertext, 0));
|
||||
byte[] truncated = decrypt(encrypted.spec, KEY, encrypted.iv,
|
||||
Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length - 1));
|
||||
byte[] trailing = decrypt(encrypted.spec, KEY, encrypted.iv,
|
||||
Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length + 1));
|
||||
|
||||
assertFalse(Arrays.equals(plaintext, wrongIv));
|
||||
assertFalse(Arrays.equals(plaintext, wrongKey));
|
||||
assertFalse(Arrays.equals(plaintext, modified));
|
||||
assertEquals(plaintext.length - 1, truncated.length);
|
||||
assertEquals(plaintext.length + 1, trailing.length);
|
||||
assertThrows(IOException.class, () -> decrypt(encrypted.spec, KEY,
|
||||
Arrays.copyOf(encrypted.iv, encrypted.iv.length - 1), encrypted.ciphertext));
|
||||
assertThrows(IOException.class, () -> decrypt(encrypted.spec, KEY, null, encrypted.ciphertext));
|
||||
System.out.println("...cases=7");
|
||||
System.out.println("ctrTamperMatrix...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cbcNoPaddingMatrix() throws Exception {
|
||||
System.out.println("cbcNoPaddingMatrix");
|
||||
AesSpec spec = AesSpec.builder().mode(AesSpec.Mode.CBC).padding(AesSpec.Padding.NOPADDING).build();
|
||||
byte[] plaintext = repeated((byte) 0x44, 32);
|
||||
Encrypted encrypted = encrypt(spec, plaintext);
|
||||
|
||||
assertFalse(Arrays.equals(plaintext,
|
||||
decrypt(spec, KEY, changed(encrypted.iv, 0), encrypted.ciphertext)));
|
||||
assertFalse(Arrays.equals(plaintext,
|
||||
decrypt(spec, WRONG_KEY, encrypted.iv, encrypted.ciphertext)));
|
||||
assertFalse(Arrays.equals(plaintext,
|
||||
decrypt(spec, KEY, encrypted.iv, changed(encrypted.ciphertext, 0))));
|
||||
assertThrows(IOException.class, () -> decrypt(spec, KEY, encrypted.iv,
|
||||
Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length - 1)));
|
||||
assertThrows(IOException.class, () -> decrypt(spec, KEY, encrypted.iv,
|
||||
Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length + 1)));
|
||||
assertThrows(IOException.class, () -> decrypt(spec, KEY,
|
||||
Arrays.copyOf(encrypted.iv, encrypted.iv.length - 1), encrypted.ciphertext));
|
||||
assertThrows(IOException.class, () -> decrypt(spec, KEY, null, encrypted.ciphertext));
|
||||
System.out.println("...cases=7");
|
||||
System.out.println("cbcNoPaddingMatrix...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cbcPkcsPaddingMatrix() throws Exception {
|
||||
System.out.println("cbcPkcsPaddingMatrix");
|
||||
AesSpec spec = AesSpec.cbcPkcs7(null);
|
||||
Encrypted encrypted = encrypt(spec, repeated((byte) 0x66, 31));
|
||||
|
||||
assertThrows(IOException.class, () -> decrypt(spec, KEY, encrypted.iv,
|
||||
Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length - 1)));
|
||||
// CBC is malleable: flipping this bit changes the final 0x01 padding byte to
|
||||
// 0x00 deterministically. The assertion covers padding rejection, not
|
||||
// authentication.
|
||||
assertThrows(IOException.class, () -> decrypt(spec, KEY, encrypted.iv,
|
||||
changed(encrypted.ciphertext, encrypted.ciphertext.length - 17)));
|
||||
assertThrows(IOException.class, () -> decrypt(spec, KEY,
|
||||
Arrays.copyOf(encrypted.iv, encrypted.iv.length - 1), encrypted.ciphertext));
|
||||
assertThrows(IOException.class, () -> decrypt(spec, KEY, null, encrypted.ciphertext));
|
||||
System.out.println("...cases=4");
|
||||
System.out.println("cbcPkcsPaddingMatrix...ok");
|
||||
}
|
||||
|
||||
private static Encrypted encrypt(AesSpec spec, byte[] plaintext) throws Exception {
|
||||
CtxInterface context = newContext("aes-security-enc-");
|
||||
EncryptionContext encryption = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.ENCRYPT, KEY, spec);
|
||||
((ContextAware) encryption).setContext(context);
|
||||
byte[] ciphertext;
|
||||
try (InputStream stream = encryption.attach(new ByteArrayInputStream(plaintext))) {
|
||||
ciphertext = stream.readAllBytes();
|
||||
} finally {
|
||||
encryption.close();
|
||||
}
|
||||
return new Encrypted(spec, context.get(ConfluxKeys.iv("AES")).clone(), ciphertext);
|
||||
}
|
||||
|
||||
private static byte[] decrypt(AesSpec spec, SecretKey key, byte[] iv, byte[] ciphertext) throws Exception {
|
||||
CtxInterface context = newContext("aes-security-dec-");
|
||||
if (iv != null) {
|
||||
context.put(ConfluxKeys.iv("AES"), iv);
|
||||
}
|
||||
EncryptionContext decryption = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) decryption).setContext(context);
|
||||
try (InputStream stream = decryption.attach(new ByteArrayInputStream(ciphertext))) {
|
||||
return stream.readAllBytes();
|
||||
} finally {
|
||||
decryption.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static FailureResult failedDecryption(AesSpec spec, SecretKey key, byte[] iv, byte[] ciphertext)
|
||||
throws Exception {
|
||||
CtxInterface context = newContext("aes-security-fail-");
|
||||
context.put(ConfluxKeys.iv("AES"), iv);
|
||||
EncryptionContext decryption = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) decryption).setContext(context);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
try (InputStream stream = decryption.attach(new ByteArrayInputStream(ciphertext))) {
|
||||
byte[] buffer = new byte[17];
|
||||
IOException failure = assertThrows(IOException.class, () -> {
|
||||
int count;
|
||||
while ((count = stream.read(buffer)) >= 0) {
|
||||
if (count > 0) {
|
||||
output.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
});
|
||||
return new FailureResult(output.size(), failure);
|
||||
} finally {
|
||||
decryption.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertAuthenticationFailure(IOException failure) {
|
||||
assertTrue(hasCause(failure, BadPaddingException.class),
|
||||
"GCM failure must retain an authentication-related provider cause");
|
||||
}
|
||||
|
||||
private static boolean hasCause(Throwable failure, Class<? extends Throwable> expectedType) {
|
||||
for (Throwable cause = failure; cause != null; cause = cause.getCause()) {
|
||||
if (expectedType.isInstance(cause)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static CtxInterface newContext(String prefix) {
|
||||
return Ctx.INSTANCE.getContext(prefix + CONTEXT_IDS.incrementAndGet());
|
||||
}
|
||||
|
||||
private static byte[] changed(byte[] input, int index) {
|
||||
byte[] result = input.clone();
|
||||
result[index] ^= 0x01;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] removed(byte[] input, int index) {
|
||||
byte[] result = new byte[input.length - 1];
|
||||
System.arraycopy(input, 0, result, 0, index);
|
||||
System.arraycopy(input, index + 1, result, index, input.length - index - 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] repeated(byte value, int length) {
|
||||
byte[] result = new byte[length];
|
||||
Arrays.fill(result, value);
|
||||
return result;
|
||||
}
|
||||
|
||||
private record Encrypted(AesSpec spec, byte[] iv, byte[] ciphertext) {}
|
||||
|
||||
private record FailureResult(int outputBytes, IOException failure) {}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public class AesGcmCrossCheckTest {
|
||||
|
||||
// --- key (either via your builder or direct JCA; both fine) ---
|
||||
CryptoAlgorithm aesAlg = CryptoAlgorithms.require("AES");
|
||||
SecretKey key = aesAlg.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256());
|
||||
SecretKey key = aesAlg.symmetricKeyGenerator(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256());
|
||||
// or:
|
||||
// KeyGenerator kg = KeyGenerator.getInstance("AES");
|
||||
// kg.init(256);
|
||||
@@ -148,7 +148,7 @@ public class AesGcmCrossCheckTest {
|
||||
AesSpec spec = AesSpec.gcm128(null);
|
||||
|
||||
// === STREAM ENCRYPT ===
|
||||
EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.ENCRYPT, key, spec);
|
||||
((ContextAware) enc).setContext(session);
|
||||
byte[] ct_stream = readAll(enc.attach(new ByteArrayInputStream(msg)));
|
||||
enc.close();
|
||||
@@ -160,7 +160,7 @@ public class AesGcmCrossCheckTest {
|
||||
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);
|
||||
EncryptionContext dec1 = new zeroecho.sdk.ZeroEchoSession().createContext("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();
|
||||
|
||||
@@ -109,18 +109,18 @@ public class AesLargeDataTest {
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
CryptoAlgorithm aes = CryptoAlgorithms.require("AES");
|
||||
|
||||
SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256());
|
||||
SecretKey key = aes.symmetricKeyGenerator(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);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) dec).setContext(session);
|
||||
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
|
||||
dec.close();
|
||||
@@ -139,18 +139,18 @@ public class AesLargeDataTest {
|
||||
System.out.printf("...input: %d bytes%n", msg.length);
|
||||
CryptoAlgorithm aes = CryptoAlgorithms.require("AES");
|
||||
|
||||
SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256());
|
||||
SecretKey key = aes.symmetricKeyGenerator(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);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) dec).setContext(session);
|
||||
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
|
||||
dec.close();
|
||||
@@ -169,18 +169,18 @@ public class AesLargeDataTest {
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
CryptoAlgorithm aes = CryptoAlgorithms.require("AES");
|
||||
|
||||
SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256());
|
||||
SecretKey key = aes.symmetricKeyGenerator(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);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) dec).setContext(session);
|
||||
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
|
||||
dec.close();
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.aes;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import conflux.Ctx;
|
||||
import conflux.CtxInterface;
|
||||
import zeroecho.core.ConfluxKeys;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.spi.ContextAware;
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
|
||||
class AesRandomSupportTest {
|
||||
private static final SecretKey KEY = new SecretKeySpec(new byte[16], "AES");
|
||||
|
||||
@Test
|
||||
void allFactoriesUseSharedRandomSource() throws Exception {
|
||||
System.out.print("AesRandomSupport/allFactoriesUseSharedRandomSource...");
|
||||
AesAlgorithm algorithm = new AesAlgorithm();
|
||||
|
||||
assertSharedRandom(algorithm.createContext(KeyUsage.ENCRYPT, KEY, AesSpec.gcm128(null)));
|
||||
assertSharedRandom(algorithm.createContext(KeyUsage.DECRYPT, KEY, AesSpec.gcm128(null)));
|
||||
assertSharedRandom(algorithm.createContext(KeyUsage.ENCRYPT, KEY, VoidSpec.INSTANCE));
|
||||
assertSharedRandom(algorithm.createContext(KeyUsage.DECRYPT, KEY, VoidSpec.INSTANCE));
|
||||
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullFallbackUsesSharedRandomSource() throws Exception {
|
||||
System.out.print("AesRandomSupport/nullFallbackUsesSharedRandomSource...");
|
||||
AesCipherContext context = new AesCipherContext(new AesAlgorithm(), KEY, true, AesSpec.gcm128(null), null);
|
||||
|
||||
assertSharedRandom(context);
|
||||
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyGenerationUsesEstablishedRandomSourcePath() throws Exception {
|
||||
System.out.print("AesRandomSupport/keyGenerationUsesEstablishedRandomSourcePath...");
|
||||
AesAlgorithm algorithm = new AesAlgorithm();
|
||||
SecretKey generated = algorithm.symmetricKeyGenerator(AesKeyGenSpec.class)
|
||||
.generateSecret(AesKeyGenSpec.aes128());
|
||||
|
||||
assertNotNull(generated);
|
||||
assertEquals("AES", generated.getAlgorithm());
|
||||
assertEquals(16, generated.getEncoded().length);
|
||||
System.out.println("...keyBytes=" + generated.getEncoded().length);
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void controlledRandomProducesFreshIvForEachDirectContext() throws Exception {
|
||||
System.out.print("AesRandomSupport/controlledRandomProducesFreshIvForEachDirectContext...");
|
||||
CountingSecureRandom random = new CountingSecureRandom();
|
||||
AesAlgorithm algorithm = new AesAlgorithm();
|
||||
CtxInterface firstSession = Ctx.INSTANCE.getContext("aes-controlled-first");
|
||||
CtxInterface secondSession = Ctx.INSTANCE.getContext("aes-controlled-second");
|
||||
|
||||
attachEmpty(new AesCipherContext(algorithm, KEY, true, AesSpec.gcm128(null), random), firstSession);
|
||||
attachEmpty(new AesCipherContext(algorithm, KEY, true, AesSpec.gcm128(null), random), secondSession);
|
||||
|
||||
byte[] firstIv = firstSession.get(ConfluxKeys.iv("AES"));
|
||||
byte[] secondIv = secondSession.get(ConfluxKeys.iv("AES"));
|
||||
assertArrayEquals(repeated((byte) 1, 12), firstIv);
|
||||
assertArrayEquals(repeated((byte) 2, 12), secondIv);
|
||||
assertNotSame(firstIv, secondIv);
|
||||
System.out.println("...randomCalls=" + random.calls);
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentFactoryConstructionUsesEstablishedSharedSource() throws Exception {
|
||||
System.out.print("AesRandomSupport/concurrentFactoryConstructionUsesEstablishedSharedSource...");
|
||||
AesAlgorithm algorithm = new AesAlgorithm();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
List<Callable<SecureRandom>> tasks = new ArrayList<>();
|
||||
for (int index = 0; index < 64; index++) {
|
||||
tasks.add(() -> randomOf(algorithm.createContext(KeyUsage.ENCRYPT, KEY, AesSpec.gcm128(null))));
|
||||
}
|
||||
for (java.util.concurrent.Future<SecureRandom> result : executor.invokeAll(tasks)) {
|
||||
assertSame(RandomSupport.getRandom(), result.get());
|
||||
}
|
||||
} finally {
|
||||
executor.close();
|
||||
}
|
||||
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
private static void assertSharedRandom(EncryptionContext context) throws Exception {
|
||||
assertSame(RandomSupport.getRandom(), randomOf(context));
|
||||
}
|
||||
|
||||
private static SecureRandom randomOf(Object context) throws Exception {
|
||||
Field field = AesCipherContext.class.getDeclaredField("rnd");
|
||||
field.setAccessible(true);
|
||||
return (SecureRandom) field.get(context);
|
||||
}
|
||||
|
||||
private static void attachEmpty(AesCipherContext context, CtxInterface session) throws Exception {
|
||||
((ContextAware) context).setContext(session);
|
||||
try (java.io.InputStream stream = context.attach(new ByteArrayInputStream(new byte[0]))) {
|
||||
stream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] repeated(byte value, int length) {
|
||||
byte[] result = new byte[length];
|
||||
java.util.Arrays.fill(result, value);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final class CountingSecureRandom extends SecureRandom {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private int calls;
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
calls++;
|
||||
java.util.Arrays.fill(bytes, (byte) calls);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,17 +116,17 @@ public class ChaChaLargeDataTest {
|
||||
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20");
|
||||
SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
|
||||
SecretKey key = chacha.symmetricKeyGenerator(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);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) dec).setContext(session);
|
||||
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
|
||||
dec.close();
|
||||
@@ -143,17 +143,17 @@ public class ChaChaLargeDataTest {
|
||||
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20");
|
||||
SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
|
||||
SecretKey key = chacha.symmetricKeyGenerator(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);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) dec).setContext(session);
|
||||
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
|
||||
dec.close();
|
||||
@@ -174,14 +174,14 @@ public class ChaChaLargeDataTest {
|
||||
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20");
|
||||
SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
|
||||
SecretKey key = chacha.symmetricKeyGenerator(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);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.ENCRYPT, key, spec);
|
||||
((ContextAware) enc).setContext(encCtx);
|
||||
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
|
||||
enc.close();
|
||||
@@ -194,7 +194,7 @@ public class ChaChaLargeDataTest {
|
||||
decCtxOk.put(ConfluxKeys.iv("CHACHA20"), nonce);
|
||||
decCtxOk.put(ConfluxKeys.tagBits("CHACHA20"), 7);
|
||||
|
||||
EncryptionContext decOk = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec);
|
||||
EncryptionContext decOk = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) decOk).setContext(decCtxOk);
|
||||
byte[] ptOk = readAll(decOk.attach(new ByteArrayInputStream(ct)));
|
||||
decOk.close();
|
||||
@@ -205,7 +205,7 @@ public class ChaChaLargeDataTest {
|
||||
decCtxBad.put(ConfluxKeys.iv("CHACHA20"), nonce);
|
||||
decCtxBad.put(ConfluxKeys.tagBits("CHACHA20"), 8);
|
||||
|
||||
EncryptionContext decBad = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec);
|
||||
EncryptionContext decBad = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) decBad).setContext(decCtxBad);
|
||||
byte[] ptBad = readAll(decBad.attach(new ByteArrayInputStream(ct)));
|
||||
decBad.close();
|
||||
@@ -228,19 +228,19 @@ public class ChaChaLargeDataTest {
|
||||
byte[] aad = "associated-data-ctx-only".getBytes();
|
||||
|
||||
CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305");
|
||||
SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
|
||||
SecretKey key = aead.symmetricKeyGenerator(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);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) dec).setContext(session);
|
||||
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
|
||||
dec.close();
|
||||
@@ -259,19 +259,19 @@ public class ChaChaLargeDataTest {
|
||||
byte[] aad = "associated-data-header".getBytes();
|
||||
|
||||
CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305");
|
||||
SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
|
||||
SecretKey key = aead.symmetricKeyGenerator(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);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) dec).setContext(session);
|
||||
byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct)));
|
||||
dec.close();
|
||||
@@ -294,7 +294,7 @@ public class ChaChaLargeDataTest {
|
||||
byte[] aadDec = "aad-dec-different".getBytes();
|
||||
|
||||
CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305");
|
||||
SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
|
||||
SecretKey key = aead.symmetricKeyGenerator(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256());
|
||||
|
||||
// Encrypt with AAD = aadEnc (ctx-only, no header)
|
||||
CtxInterface encCtx = Ctx.INSTANCE.getContext("chacha-aead-enc-" + System.nanoTime());
|
||||
@@ -302,7 +302,7 @@ public class ChaChaLargeDataTest {
|
||||
|
||||
ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build();
|
||||
|
||||
EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec);
|
||||
((ContextAware) enc).setContext(encCtx);
|
||||
byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg)));
|
||||
enc.close();
|
||||
@@ -314,7 +314,7 @@ public class ChaChaLargeDataTest {
|
||||
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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec);
|
||||
((ContextAware) dec).setContext(decCtx);
|
||||
|
||||
assertThrows(IOException.class, () -> {
|
||||
|
||||
@@ -62,7 +62,9 @@ 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.core.KeyOperation;
|
||||
import zeroecho.core.KeyOperationInfo;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
public class AgreementAlgorithmsRoundTripTest {
|
||||
@@ -174,7 +176,7 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
|
||||
ContextSpec spec = null;
|
||||
try {
|
||||
spec = cap.defaultSpec().get();
|
||||
spec = cap.defaultSpec();
|
||||
} catch (Throwable ignore) {
|
||||
spec = tryExtractContextSpec(alg);
|
||||
}
|
||||
@@ -204,8 +206,8 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
MessageAgreementContext bCtx = null;
|
||||
|
||||
try {
|
||||
aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, aliceKey, spec);
|
||||
bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bobKey, spec);
|
||||
aCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, aliceKey, spec);
|
||||
bCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, bobKey, spec);
|
||||
|
||||
byte[] aMsg = aCtx.getPeerMessage();
|
||||
byte[] bMsg = bCtx.getPeerMessage();
|
||||
@@ -272,11 +274,11 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
|
||||
try {
|
||||
// Alice (initiator): has Bob's public key
|
||||
aliceCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPublic(),
|
||||
aliceCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, bob.getPublic(),
|
||||
VoidSpec.INSTANCE);
|
||||
|
||||
// Bob (responder): has his private key
|
||||
bobCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(),
|
||||
bobCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(),
|
||||
VoidSpec.INSTANCE);
|
||||
|
||||
// Initiator produces encapsulation message (ciphertext) to send
|
||||
@@ -324,7 +326,7 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
|
||||
ContextSpec spec = null;
|
||||
try {
|
||||
spec = cap.defaultSpec().get();
|
||||
spec = cap.defaultSpec();
|
||||
} catch (Throwable ignore) {
|
||||
spec = tryExtractContextSpec(alg);
|
||||
}
|
||||
@@ -360,8 +362,8 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
AgreementContext bCtx = null;
|
||||
|
||||
try {
|
||||
aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(), spec);
|
||||
bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), spec);
|
||||
aCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(), spec);
|
||||
bCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), spec);
|
||||
|
||||
aCtx.setPeerPublic(bob.getPublic());
|
||||
bCtx.setPeerPublic(alice.getPublic());
|
||||
@@ -402,14 +404,14 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
// ----- helpers -----
|
||||
private static KeyPair generateKeyPair(CryptoAlgorithm alg) {
|
||||
try {
|
||||
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
|
||||
if (bi.defaultKeySpec == null) {
|
||||
for (KeyOperationInfo bi : alg.keyOperations()) {
|
||||
if (bi.operation() != KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE || bi.defaultSpec() == null) {
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType;
|
||||
AsymmetricKeyBuilder<AlgorithmKeySpec> builder = alg.asymmetricKeyBuilder(specType);
|
||||
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
|
||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType();
|
||||
AsymmetricKeyPairGenerator<AlgorithmKeySpec> builder = alg.asymmetricKeyPairGenerator(specType);
|
||||
AlgorithmKeySpec spec = bi.defaultSpec();
|
||||
return builder.generateKeyPair(spec);
|
||||
}
|
||||
} catch (Throwable ignore) {
|
||||
@@ -423,7 +425,7 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
for (Capability c : alg.listCapabilities()) {
|
||||
if (c.role() == KeyUsage.AGREEMENT && ContextSpec.class.isAssignableFrom(c.specType())) {
|
||||
try {
|
||||
return c.defaultSpec().get();
|
||||
return c.defaultSpec();
|
||||
} catch (Throwable ignore) {
|
||||
// continue searching
|
||||
}
|
||||
|
||||
@@ -132,10 +132,10 @@ public class EcdsaLargeDataTest {
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
// Key pair via your unified ECDSA algorithm and enum spec
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("ECDSA", spec);
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ECDSA", spec);
|
||||
|
||||
// SIGN (streaming): emits [body][signature]; capture trailer
|
||||
SignatureContext signer = CryptoAlgorithms.create("ECDSA", KeyUsage.SIGN, kp.getPrivate(), spec);
|
||||
SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("ECDSA", KeyUsage.SIGN, kp.getPrivate(), spec);
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
final int sigLen = signer.tagLength(); // should equal spec.signFixedLength()
|
||||
|
||||
@@ -160,7 +160,7 @@ public class EcdsaLargeDataTest {
|
||||
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);
|
||||
SignatureContext verifier = new zeroecho.sdk.ZeroEchoSession().createContext("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec);
|
||||
verifier.setExpectedTag(ourSig);
|
||||
byte[] sink2;
|
||||
try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
@@ -181,7 +181,7 @@ public class EcdsaLargeDataTest {
|
||||
// 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);
|
||||
SignatureContext verifier2 = new zeroecho.sdk.ZeroEchoSession().createContext("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec);
|
||||
verifier2.setExpectedTag(jcaSig);
|
||||
try (InputStream verIn2 = verifier2.wrap(new ByteArrayInputStream(msg))) {
|
||||
byte[] passthrough = readAll(verIn2);
|
||||
|
||||
@@ -132,11 +132,11 @@ public class Ed25519LargeDataTest {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
|
||||
|
||||
// SIGN: context emits [body][signature] — capture trailer via
|
||||
// TailStrippingInputStream
|
||||
SignatureContext signer = CryptoAlgorithms.create("Ed25519", KeyUsage.SIGN, kp.getPrivate(), null);
|
||||
SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("Ed25519", KeyUsage.SIGN, kp.getPrivate(), null);
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
final int sigLen = signer.tagLength();
|
||||
|
||||
@@ -166,7 +166,7 @@ public class Ed25519LargeDataTest {
|
||||
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);
|
||||
SignatureContext verifier = new zeroecho.sdk.ZeroEchoSession().createContext("Ed25519", KeyUsage.VERIFY, kp.getPublic(), null);
|
||||
verifier.setExpectedTag(ourSig);
|
||||
|
||||
byte[] sink2;
|
||||
|
||||
@@ -132,11 +132,11 @@ public class Ed448LargeDataTest {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("Ed448", Ed448KeyGenSpec.defaultSpec());
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Ed448", Ed448KeyGenSpec.defaultSpec());
|
||||
|
||||
// SIGN: context emits [body][signature] — capture trailer via
|
||||
// TailStrippingInputStream
|
||||
SignatureContext signer = CryptoAlgorithms.create("Ed448", KeyUsage.SIGN, kp.getPrivate(), null);
|
||||
SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("Ed448", KeyUsage.SIGN, kp.getPrivate(), null);
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
final int sigLen = signer.tagLength();
|
||||
|
||||
@@ -166,7 +166,7 @@ public class Ed448LargeDataTest {
|
||||
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);
|
||||
SignatureContext verifier = new zeroecho.sdk.ZeroEchoSession().createContext("Ed448", KeyUsage.VERIFY, kp.getPublic(), null);
|
||||
verifier.setExpectedTag(ourSig);
|
||||
|
||||
byte[] sink2;
|
||||
|
||||
@@ -93,16 +93,16 @@ public class ElgamalLargeDataTest {
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
System.out.printf("...input: %d bytes%n", msg.length);
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
ElgamalEncSpec spec = ElgamalEncSpec.pkcs1();
|
||||
|
||||
EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec);
|
||||
InputStream ptIn = dec.attach(new ByteArrayInputStream(ct));
|
||||
byte[] pt = ptIn.readAllBytes();
|
||||
dec.close();
|
||||
@@ -121,16 +121,16 @@ public class ElgamalLargeDataTest {
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
System.out.printf("...input: %d bytes%n", msg.length);
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
ElgamalEncSpec spec = ElgamalEncSpec.noPadding();
|
||||
|
||||
EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec);
|
||||
InputStream ptIn = dec.attach(new ByteArrayInputStream(ct));
|
||||
byte[] pt = ptIn.readAllBytes();
|
||||
dec.close();
|
||||
@@ -149,10 +149,10 @@ public class ElgamalLargeDataTest {
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
System.out.printf("...input: %d bytes%n", msg.length);
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
ElgamalEncSpec spec = ElgamalEncSpec.noPadding();
|
||||
|
||||
EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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");
|
||||
|
||||
@@ -127,11 +127,11 @@ public class HmacLargeDataTest {
|
||||
CryptoAlgorithm algo = CryptoAlgorithms.require(ALG_ID);
|
||||
|
||||
// Generate a key (macName must match)
|
||||
SecretKey key = algo.symmetricKeyBuilder(HmacKeyGenSpec.class).generateSecret(new HmacKeyGenSpec(JCA_MAC, 256));
|
||||
SecretKey key = algo.symmetricKeyGenerator(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);
|
||||
MacContext mac = new zeroecho.sdk.ZeroEchoSession().createContext(ALG_ID, KeyUsage.MAC, key, spec);
|
||||
final byte[][] tagHolder = new byte[1][];
|
||||
final int tagLen = mac.tagLength();
|
||||
|
||||
@@ -161,7 +161,7 @@ public class HmacLargeDataTest {
|
||||
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);
|
||||
MacContext ver = new zeroecho.sdk.ZeroEchoSession().createContext(ALG_ID, KeyUsage.MAC, key, spec);
|
||||
ver.setExpectedTag(tag);
|
||||
byte[] pass2;
|
||||
try (InputStream in = ver.wrap(new ByteArrayInputStream(msg))) {
|
||||
|
||||
@@ -117,13 +117,13 @@ public final class MldsaLargeDataTest {
|
||||
String caseId = "ML-DSA " + ps.name() + " preHash=" + preHash.name();
|
||||
System.out.println(INDENT + " case=" + safeText(caseId));
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("ML-DSA", spec);
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-DSA", spec);
|
||||
|
||||
SignatureContext mldsaVerifier = CryptoAlgorithms.create("ML-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
SignatureContext mldsaVerifier = new zeroecho.sdk.ZeroEchoSession().createContext("ML-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
int expectedSigLen = mldsaVerifier.tagLength();
|
||||
System.out.println(INDENT + " expectedSigLen=" + expectedSigLen);
|
||||
|
||||
SignatureContext signer = CryptoAlgorithms.create("ML-DSA", KeyUsage.SIGN, kp.getPrivate());
|
||||
SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("ML-DSA", KeyUsage.SIGN, kp.getPrivate());
|
||||
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
byte[] passthrough;
|
||||
@@ -178,7 +178,7 @@ public final class MldsaLargeDataTest {
|
||||
byte[] badSig = Arrays.copyOf(signature, signature.length);
|
||||
badSig[0] = (byte) (badSig[0] ^ 0x01);
|
||||
|
||||
SignatureContext badVerifier = CryptoAlgorithms.create("ML-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
SignatureContext badVerifier = new zeroecho.sdk.ZeroEchoSession().createContext("ML-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
|
||||
try {
|
||||
badVerifier.setExpectedTag(badSig);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.rsa;
|
||||
|
||||
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.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyPair;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
|
||||
class BlockGeometryTest {
|
||||
@Test
|
||||
void geometryValidation() {
|
||||
System.out.println("geometryValidation");
|
||||
assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(1, 1, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(2, 0, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(3, 2, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(2, 2, -1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(2, 2, 1));
|
||||
|
||||
BlockGeometry maximum = new BlockGeometry(Integer.MAX_VALUE, Integer.MAX_VALUE, 0);
|
||||
assertEquals(Integer.MAX_VALUE, maximum.inChunkSize());
|
||||
assertEquals(Integer.MAX_VALUE, maximum.outChunkSize());
|
||||
System.out.println("...maximum=" + maximum.inChunkSize());
|
||||
System.out.println("geometryValidation...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void valueSemantics() {
|
||||
System.out.println("valueSemantics");
|
||||
BlockGeometry first = new BlockGeometry(2, 3, 0);
|
||||
BlockGeometry equal = new BlockGeometry(2, 3, 0);
|
||||
BlockGeometry different = new BlockGeometry(2, 4, 0);
|
||||
|
||||
assertEquals(first, equal);
|
||||
assertEquals(first.hashCode(), equal.hashCode());
|
||||
assertNotEquals(first, different);
|
||||
assertEquals(2, first.inChunkSize());
|
||||
assertEquals(3, first.outChunkSize());
|
||||
assertEquals(0, first.finalizationOutputChunks());
|
||||
assertEquals("BlockGeometry[inChunkSize=2, outChunkSize=3, finalizationOutputChunks=0]",
|
||||
first.toString());
|
||||
System.out.println("...legacyFields=true");
|
||||
System.out.println("valueSemantics...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualRsaPath() throws Exception {
|
||||
System.out.println("actualRsaPath");
|
||||
KeyPair keyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
RsaEncSpec spec = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256);
|
||||
BlockGeometry encryptGeometry = BlockGeometry.forRsa(spec, keyPair.getPublic(), true);
|
||||
BlockGeometry decryptGeometry = BlockGeometry.forRsa(spec, keyPair.getPrivate(), false);
|
||||
|
||||
assertEquals(190, encryptGeometry.inChunkSize());
|
||||
assertEquals(256, encryptGeometry.outChunkSize());
|
||||
assertEquals(256, decryptGeometry.inChunkSize());
|
||||
assertEquals(256, decryptGeometry.outChunkSize());
|
||||
|
||||
EncryptionContext context = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, keyPair.getPublic(), spec);
|
||||
try (InputStream stream = context.attach(new ByteArrayInputStream(new byte[] { 1, 2, 3 }))) {
|
||||
assertEquals(256, stream.readAllBytes().length);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
System.out.println("...ciphertextBytes=256");
|
||||
System.out.println("actualRsaPath...ok");
|
||||
}
|
||||
}
|
||||
@@ -89,16 +89,16 @@ public class RsaLargeDataTest {
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
System.out.printf("...input: %d bytes%n", msg.length);
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
RsaEncSpec spec = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256);
|
||||
|
||||
EncryptionContext enc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec);
|
||||
InputStream ptIn = dec.attach(new ByteArrayInputStream(ct));
|
||||
byte[] pt = ptIn.readAllBytes();
|
||||
dec.close();
|
||||
@@ -117,16 +117,16 @@ public class RsaLargeDataTest {
|
||||
byte[] msg = randomBytes(SIZE);
|
||||
System.out.printf("...input: %d bytes%n", msg.length);
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
RsaEncSpec spec = RsaEncSpec.pkcs1v15();
|
||||
|
||||
EncryptionContext enc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec);
|
||||
EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("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);
|
||||
EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec);
|
||||
InputStream ptIn = dec.attach(new ByteArrayInputStream(ct));
|
||||
byte[] pt = ptIn.readAllBytes();
|
||||
dec.close();
|
||||
|
||||
@@ -122,18 +122,18 @@ public final class SlhDsaLargeDataTest {
|
||||
String caseId = "SLH-DSA " + hash.name() + " " + sec.name() + " " + variant.name();
|
||||
System.out.println(INDENT + " case=" + safeText(caseId));
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("SLH-DSA", spec);
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("SLH-DSA", spec);
|
||||
|
||||
// Create verifier FIRST to obtain tag length via
|
||||
// SlhDsaSignatureContext.sigLenFromPublicKey.
|
||||
SignatureContext verifierCtx = CryptoAlgorithms.create("SLH-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
SignatureContext verifierCtx = new zeroecho.sdk.ZeroEchoSession().createContext("SLH-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
|
||||
int expectedSigLen = verifierCtx.tagLength();
|
||||
System.out.println(INDENT + " expectedSigLen=" + expectedSigLen);
|
||||
|
||||
// Now sign and strip trailer using the expected length from verifier (not from
|
||||
// signer).
|
||||
SignatureContext signer = CryptoAlgorithms.create("SLH-DSA", KeyUsage.SIGN, kp.getPrivate());
|
||||
SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("SLH-DSA", KeyUsage.SIGN, kp.getPrivate());
|
||||
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
byte[] passthrough;
|
||||
@@ -188,7 +188,7 @@ public final class SlhDsaLargeDataTest {
|
||||
byte[] badSig = Arrays.copyOf(signature, signature.length);
|
||||
badSig[0] = (byte) (badSig[0] ^ 0x01);
|
||||
|
||||
SignatureContext badVerifier = CryptoAlgorithms.create("SLH-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
SignatureContext badVerifier = new zeroecho.sdk.ZeroEchoSession().createContext("SLH-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
|
||||
try {
|
||||
badVerifier.setExpectedTag(badSig);
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.audit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.Key;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.NullKey;
|
||||
import zeroecho.core.alg.aes.AesAlgorithm;
|
||||
import zeroecho.core.context.CryptoContext;
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
|
||||
|
||||
public class AuditedContextsAccessorTest {
|
||||
@Test
|
||||
void specAccessorBinding() {
|
||||
System.out.println("specAccessorBinding");
|
||||
MetadataListener firstListener = new MetadataListener();
|
||||
MetadataListener secondListener = new MetadataListener();
|
||||
MetadataListener missingListener = new MetadataListener();
|
||||
MetadataListener incompatibleListener = new MetadataListener();
|
||||
|
||||
AuditedContexts.wrap(new FirstSpecDigest(), firstListener, KeyUsage.DIGEST);
|
||||
AuditedContexts.wrap(new SecondSpecDigest(), secondListener, KeyUsage.DIGEST);
|
||||
AuditedContexts.wrap(new BaseDigest(), missingListener, KeyUsage.DIGEST);
|
||||
AuditedContexts.wrap(new IncompatibleAccessorsContext(), incompatibleListener, KeyUsage.ENCRYPT);
|
||||
|
||||
assertNotNull(firstListener.specMeta);
|
||||
assertNotNull(secondListener.specMeta);
|
||||
assertNull(missingListener.specMeta);
|
||||
assertNull(incompatibleListener.specMeta);
|
||||
assertEquals("default", firstListener.provider);
|
||||
assertEquals("default", secondListener.provider);
|
||||
System.out.println("...runtimeClasses=4");
|
||||
System.out.println("specAccessorBinding...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tagLengthAccessorBinding() throws Exception {
|
||||
System.out.println("tagLengthAccessorBinding");
|
||||
BaseDigest explicitTarget = new BaseDigest();
|
||||
DigestContext explicit = (DigestContext) AuditedContexts.wrap(explicitTarget, AuditListener.noop(),
|
||||
KeyUsage.DIGEST);
|
||||
assertEquals(4, explicit.tagLength());
|
||||
assertEquals(4, explicit.tagLength());
|
||||
explicit.wrap(new ByteArrayInputStream(new byte[] { 1 })).readAllBytes();
|
||||
assertEquals(2, explicitTarget.tagCalls.get());
|
||||
|
||||
BaseDigest lazyTarget = new BaseDigest();
|
||||
DigestContext lazy = (DigestContext) AuditedContexts.wrap(lazyTarget, AuditListener.noop(), KeyUsage.DIGEST);
|
||||
lazy.wrap(new ByteArrayInputStream(new byte[] { 1 })).readAllBytes();
|
||||
lazy.wrap(new ByteArrayInputStream(new byte[] { 2 })).readAllBytes();
|
||||
assertEquals(1, lazyTarget.tagCalls.get());
|
||||
|
||||
BaseDigest concurrentTarget = new BaseDigest();
|
||||
DigestContext concurrent = (DigestContext) AuditedContexts.wrap(concurrentTarget, AuditListener.noop(),
|
||||
KeyUsage.DIGEST);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
List<java.util.concurrent.Callable<Integer>> calls = new ArrayList<>();
|
||||
for (int index = 0; index < 64; index++) {
|
||||
calls.add(concurrent::tagLength);
|
||||
}
|
||||
for (java.util.concurrent.Future<Integer> result : executor.invokeAll(calls)) {
|
||||
assertEquals(4, result.get());
|
||||
}
|
||||
} finally {
|
||||
executor.close();
|
||||
}
|
||||
assertEquals(64, concurrentTarget.tagCalls.get());
|
||||
System.out.println("...concurrentCalls=" + concurrentTarget.tagCalls.get());
|
||||
System.out.println("tagLengthAccessorBinding...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessorFailureOrder() throws Exception {
|
||||
System.out.println("accessorFailureOrder");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
IllegalStateException expected = new IllegalStateException("controlled tag failure");
|
||||
DigestContext failing = (DigestContext) AuditedContexts.wrap(new FailingDigest(expected), listener,
|
||||
KeyUsage.DIGEST);
|
||||
|
||||
IllegalStateException actual = assertThrows(IllegalStateException.class, failing::tagLength);
|
||||
assertSame(expected, actual);
|
||||
assertSame(expected, listener.failure);
|
||||
assertEquals(List.of("meta", "failure"), listener.events);
|
||||
|
||||
RecordingListener successListener = new RecordingListener();
|
||||
DigestContext successful = (DigestContext) AuditedContexts.wrap(new BaseDigest(), successListener,
|
||||
KeyUsage.DIGEST);
|
||||
successful.wrap(new ByteArrayInputStream(new byte[] { 1, 2, 3 })).readAllBytes();
|
||||
assertEquals("meta", successListener.events.get(0));
|
||||
assertEquals("tag", successListener.events.get(successListener.events.size() - 1));
|
||||
System.out.println("...failureEvents=" + listener.events.size());
|
||||
System.out.println("accessorFailureOrder...ok");
|
||||
}
|
||||
|
||||
public interface SpecAccessor {
|
||||
ContextSpec spec();
|
||||
}
|
||||
|
||||
public interface IncompatibleAccessors {
|
||||
InputStream wrap(InputStream input);
|
||||
|
||||
String spec();
|
||||
|
||||
String tagLength();
|
||||
}
|
||||
|
||||
public record TestSpec(String name) implements ContextSpec {
|
||||
}
|
||||
|
||||
public static class BaseDigest implements DigestContext {
|
||||
private final AtomicInteger tagCalls = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
public InputStream wrap(InputStream upstream) {
|
||||
return new java.io.SequenceInputStream(upstream, new ByteArrayInputStream(new byte[4]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int tagLength() {
|
||||
tagCalls.incrementAndGet();
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVerificationApproach(VerificationBiPredicate<byte[]> strategy) {
|
||||
// Not needed by this test context.
|
||||
}
|
||||
|
||||
@Override
|
||||
public VerificationBiPredicate<byte[]> getVerificationCore() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoAlgorithm algorithm() {
|
||||
return new AesAlgorithm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key key() {
|
||||
return NullKey.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// No resources.
|
||||
}
|
||||
}
|
||||
|
||||
public static final class FirstSpecDigest extends BaseDigest implements SpecAccessor {
|
||||
@Override
|
||||
public ContextSpec spec() {
|
||||
return new TestSpec("first");
|
||||
}
|
||||
}
|
||||
|
||||
public static final class SecondSpecDigest extends BaseDigest implements SpecAccessor {
|
||||
@Override
|
||||
public ContextSpec spec() {
|
||||
return new TestSpec("second");
|
||||
}
|
||||
}
|
||||
|
||||
public static final class FailingDigest extends BaseDigest {
|
||||
private final IllegalStateException failure;
|
||||
|
||||
private FailingDigest(IllegalStateException failure) {
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int tagLength() {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class IncompatibleAccessorsContext implements EncryptionContext, IncompatibleAccessors {
|
||||
@Override
|
||||
public InputStream attach(InputStream upstream) {
|
||||
return upstream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream wrap(InputStream input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String spec() {
|
||||
return "not a context spec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String tagLength() {
|
||||
return "not an integer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoAlgorithm algorithm() {
|
||||
return new AesAlgorithm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key key() {
|
||||
return NullKey.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// No resources.
|
||||
}
|
||||
}
|
||||
|
||||
private static class MetadataListener implements AuditListener {
|
||||
private String provider;
|
||||
private Map<String, Object> specMeta;
|
||||
|
||||
@Override
|
||||
public void onContextCreatedMeta(String ctxId, String algoId, String providerName, KeyUsage role,
|
||||
String keyFingerprint, Map<String, Object> metadata) {
|
||||
provider = providerName;
|
||||
specMeta = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingListener extends MetadataListener {
|
||||
private final List<String> events = new ArrayList<>();
|
||||
private Throwable failure;
|
||||
|
||||
@Override
|
||||
public void onContextCreatedMeta(String ctxId, String algoId, String providerName, KeyUsage role,
|
||||
String keyFingerprint, Map<String, Object> metadata) {
|
||||
super.onContextCreatedMeta(ctxId, algoId, providerName, role, keyFingerprint, metadata);
|
||||
events.add("meta");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String ctxId, String stage, String operation, Throwable cause) {
|
||||
failure = cause;
|
||||
events.add("failure");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagProduced(String ctxId, int tagLength, String policy) {
|
||||
events.add("tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.audit;
|
||||
|
||||
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.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.CryptoContext;
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
|
||||
class AuditedContextsRegressionTest {
|
||||
|
||||
@Test
|
||||
void unrelatedProxyGetsWrapped() throws Exception {
|
||||
System.out.println("unrelatedProxyGetsWrapped");
|
||||
DigestContext target = mock(DigestContext.class);
|
||||
when(target.tagLength()).thenReturn(2);
|
||||
when(target.wrap(any(InputStream.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
DigestContext unrelated = (DigestContext) Proxy.newProxyInstance(
|
||||
DigestContext.class.getClassLoader(), new Class<?>[] { DigestContext.class },
|
||||
(proxy, method, arguments) -> {
|
||||
try {
|
||||
return method.invoke(target, arguments);
|
||||
} catch (InvocationTargetException exception) {
|
||||
throw exception.getCause();
|
||||
}
|
||||
});
|
||||
|
||||
DigestContext wrapped = (DigestContext) AuditedContexts.wrap(unrelated, AuditListener.noop(), KeyUsage.DIGEST);
|
||||
|
||||
assertNotSame(unrelated, wrapped);
|
||||
assertTrue(Proxy.isProxyClass(wrapped.getClass()));
|
||||
assertArrayEquals(new byte[] { 1, 2 }, wrapped.wrap(new ByteArrayInputStream(new byte[] { 1, 2 })).readAllBytes());
|
||||
assertSame(wrapped, AuditedContexts.wrap(wrapped, AuditListener.noop(), KeyUsage.DIGEST));
|
||||
System.out.println("...nestedProxy=true");
|
||||
System.out.println("unrelatedProxyGetsWrapped...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyEofKeepsBodyAccounting() throws Exception {
|
||||
System.out.println("verifyEofKeepsBodyAccounting");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
DigestContext target = mock(DigestContext.class);
|
||||
when(target.tagLength()).thenReturn(8);
|
||||
when(target.wrap(any(InputStream.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
DigestContext wrapped = (DigestContext) AuditedContexts.wrap(target, listener, KeyUsage.DIGEST);
|
||||
wrapped.setExpectedTag(new byte[8]);
|
||||
|
||||
assertArrayEquals(new byte[] { 1, 2, 3 },
|
||||
wrapped.wrap(new ByteArrayInputStream(new byte[] { 1, 2, 3 })).readAllBytes());
|
||||
|
||||
assertEquals(List.of(Boolean.TRUE), listener.verificationResults);
|
||||
assertEquals(3, listener.bodyBytes);
|
||||
assertEquals(0, listener.trailerBytes);
|
||||
assertEquals(0, listener.tagEvents);
|
||||
System.out.println("...body=3...trailer=0");
|
||||
System.out.println("verifyEofKeepsBodyAccounting...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyFailureSingleEvent() throws Exception {
|
||||
System.out.println("verifyFailureSingleEvent");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
IOException expected = new IOException("controlled verification failure");
|
||||
DigestContext target = mock(DigestContext.class);
|
||||
when(target.tagLength()).thenReturn(4);
|
||||
when(target.wrap(any(InputStream.class))).thenReturn(new InputStream() {
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
throw expected;
|
||||
}
|
||||
});
|
||||
DigestContext wrapped = (DigestContext) AuditedContexts.wrap(target, listener, KeyUsage.DIGEST);
|
||||
wrapped.setExpectedTag(new byte[4]);
|
||||
|
||||
IOException actual = assertThrows(IOException.class, () -> {
|
||||
try (InputStream input = wrapped.wrap(new ByteArrayInputStream(new byte[0]))) {
|
||||
input.readAllBytes();
|
||||
}
|
||||
});
|
||||
|
||||
assertSame(expected, actual);
|
||||
assertEquals(List.of(expected), listener.failures);
|
||||
assertEquals(List.of(Boolean.FALSE), listener.verificationResults);
|
||||
System.out.println("...failures=1...verifyFalse=1");
|
||||
System.out.println("verifyFailureSingleEvent...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fingerprintsAvoidSecretBytes() {
|
||||
System.out.println("fingerprintsAvoidSecretBytes");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
SecretKey secretKey = mock(SecretKey.class);
|
||||
when(secretKey.getAlgorithm()).thenReturn("CONTROLLED-SECRET");
|
||||
PrivateKey privateKey = mock(PrivateKey.class);
|
||||
when(privateKey.getAlgorithm()).thenReturn("CONTROLLED-PRIVATE");
|
||||
|
||||
EncryptionContext secretContext = mock(EncryptionContext.class);
|
||||
when(secretContext.key()).thenReturn(secretKey);
|
||||
EncryptionContext privateContext = mock(EncryptionContext.class);
|
||||
when(privateContext.key()).thenReturn(privateKey);
|
||||
AuditedContexts.wrap(secretContext, listener, KeyUsage.ENCRYPT);
|
||||
AuditedContexts.wrap(privateContext, listener, KeyUsage.DECRYPT);
|
||||
|
||||
verify(secretKey, never()).getEncoded();
|
||||
verify(privateKey, never()).getEncoded();
|
||||
assertEquals(2, listener.fingerprints.size());
|
||||
assertTrue(listener.fingerprints.get(0).startsWith("CONTROLLED-SECRET:"));
|
||||
assertTrue(listener.fingerprints.get(1).startsWith("CONTROLLED-PRIVATE:"));
|
||||
assertFalse(listener.fingerprints.toString().contains("sensitive-key-bytes"));
|
||||
|
||||
byte[] publicEncoding = { 1, 2, 3, 4 };
|
||||
PublicKey publicKey = mock(PublicKey.class);
|
||||
when(publicKey.getAlgorithm()).thenReturn("CONTROLLED-PUBLIC");
|
||||
when(publicKey.getEncoded()).thenReturn(publicEncoding);
|
||||
EncryptionContext publicContext = mock(EncryptionContext.class);
|
||||
when(publicContext.key()).thenReturn(publicKey);
|
||||
AuditedContexts.wrap(publicContext, listener, KeyUsage.ENCRYPT);
|
||||
assertArrayEquals(new byte[4], publicEncoding);
|
||||
assertFalse(listener.fingerprints.get(2).contains(Arrays.toString(new byte[] { 1, 2, 3, 4 })));
|
||||
|
||||
System.out.println("...privateEncodedCalls=0...secretEncodedCalls=0...publicEncodingCleared=true");
|
||||
System.out.println("fingerprintsAvoidSecretBytes...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void proxyStringRepresentationDoesNotDelegate() {
|
||||
System.out.println("proxyStringRepresentationDoesNotDelegate");
|
||||
SensitiveStringDigest target = new SensitiveStringDigest();
|
||||
|
||||
DigestContext wrapped = (DigestContext) AuditedContexts.wrap(target, AuditListener.noop(), KeyUsage.DIGEST);
|
||||
String description = wrapped.toString();
|
||||
|
||||
assertTrue(description.startsWith("AuditedCryptoContext["));
|
||||
assertFalse(description.contains("SENSITIVE-TARGET-STATE"));
|
||||
assertEquals(0, target.toStringCalls);
|
||||
System.out.println("...description=" + description.substring(0, Math.min(30, description.length())) + "...");
|
||||
System.out.println("proxyStringRepresentationDoesNotDelegate...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void agreementFailuresSingleEvent() {
|
||||
System.out.println("agreementFailuresSingleEvent");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
IllegalArgumentException peerFailure = new IllegalArgumentException("peer failure");
|
||||
IllegalStateException deriveFailure = new IllegalStateException("derive failure");
|
||||
AgreementContext agreement = mock(AgreementContext.class);
|
||||
doThrow(peerFailure).when(agreement).setPeerPublic(any(PublicKey.class));
|
||||
when(agreement.deriveSecret()).thenThrow(deriveFailure);
|
||||
AgreementContext wrappedAgreement = (AgreementContext) AuditedContexts.wrap(
|
||||
agreement, listener, KeyUsage.AGREEMENT);
|
||||
|
||||
assertSame(peerFailure, assertThrows(IllegalArgumentException.class,
|
||||
() -> wrappedAgreement.setPeerPublic(mock(PublicKey.class))));
|
||||
assertSame(deriveFailure, assertThrows(IllegalStateException.class, wrappedAgreement::deriveSecret));
|
||||
|
||||
IllegalArgumentException setMessageFailure = new IllegalArgumentException("set message failure");
|
||||
IllegalStateException getMessageFailure = new IllegalStateException("get message failure");
|
||||
IllegalStateException messageDeriveFailure = new IllegalStateException("message derive failure");
|
||||
MessageAgreementContext messageAgreement = mock(MessageAgreementContext.class);
|
||||
doThrow(setMessageFailure).when(messageAgreement).setPeerMessage(any(byte[].class));
|
||||
when(messageAgreement.getPeerMessage()).thenThrow(getMessageFailure);
|
||||
when(messageAgreement.deriveSecret()).thenThrow(messageDeriveFailure);
|
||||
MessageAgreementContext wrappedMessage = (MessageAgreementContext) AuditedContexts.wrap(
|
||||
messageAgreement, listener, KeyUsage.AGREEMENT);
|
||||
|
||||
assertSame(setMessageFailure, assertThrows(IllegalArgumentException.class,
|
||||
() -> wrappedMessage.setPeerMessage(new byte[] { 1 })));
|
||||
assertSame(getMessageFailure, assertThrows(IllegalStateException.class, wrappedMessage::getPeerMessage));
|
||||
assertSame(messageDeriveFailure,
|
||||
assertThrows(IllegalStateException.class, wrappedMessage::deriveSecret));
|
||||
assertEquals(List.of(peerFailure, deriveFailure, setMessageFailure, getMessageFailure, messageDeriveFailure),
|
||||
listener.failures);
|
||||
|
||||
System.out.println("...operations=5...failures=5");
|
||||
System.out.println("agreementFailuresSingleEvent...ok");
|
||||
}
|
||||
|
||||
private static final class RecordingListener implements AuditListener {
|
||||
private final List<String> fingerprints = new ArrayList<>();
|
||||
private final List<Throwable> failures = new ArrayList<>();
|
||||
private final List<Boolean> verificationResults = new ArrayList<>();
|
||||
private long bodyBytes;
|
||||
private long trailerBytes;
|
||||
private int tagEvents;
|
||||
|
||||
@Override
|
||||
public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role,
|
||||
String keyFingerprint, Map<String, Object> specMeta) {
|
||||
fingerprints.add(keyFingerprint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(String contextId, long bodyByteCount, long trailerByteCount) {
|
||||
bodyBytes = bodyByteCount;
|
||||
trailerBytes = trailerByteCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagProduced(String contextId, int tagLength, String policy) {
|
||||
tagEvents++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVerifyResult(String contextId, boolean success, String policy, String expectedSource,
|
||||
int tagLength) {
|
||||
verificationResults.add(Boolean.valueOf(success));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String contextId, String stage, String operation, Throwable cause) {
|
||||
failures.add(cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SensitiveStringDigest extends AuditedContextsAccessorTest.BaseDigest {
|
||||
private int toStringCalls;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
toStringCalls++;
|
||||
return "SENSITIVE-TARGET-STATE";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.audit;
|
||||
|
||||
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 java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class JulAuditListenerStdSecurityTest {
|
||||
private static final String SENSITIVE_TEXT = "controlled-sensitive-message";
|
||||
private static final AtomicInteger LOGGER_IDS = new AtomicInteger();
|
||||
|
||||
@Test
|
||||
void keyLoggingUsesOnlySafeMetadataAndClearsPublicEncoding() {
|
||||
System.out.println("keyLoggingUsesOnlySafeMetadataAndClearsPublicEncoding");
|
||||
RecordingHandler handler = new RecordingHandler();
|
||||
JulAuditListenerStd listener = listener(handler);
|
||||
CountingSecretKey secretKey = new CountingSecretKey();
|
||||
CountingPrivateKey privateKey = new CountingPrivateKey();
|
||||
byte[] publicEncoding = { 1, 2, 3, 4 };
|
||||
PublicKey publicKey = new ControlledPublicKey(publicEncoding);
|
||||
|
||||
listener.onKeyBuilt("secret", "provider", null, secretKey);
|
||||
listener.onKeyBuilt("private", "provider", null, privateKey);
|
||||
listener.onKeyBuilt("public", "provider", null, publicKey);
|
||||
|
||||
assertEquals(0, secretKey.encodedCalls);
|
||||
assertEquals(0, privateKey.encodedCalls);
|
||||
assertArrayEquals(new byte[publicEncoding.length], publicEncoding);
|
||||
assertFalse(handler.rendered().contains(Arrays.toString(new byte[] { 1, 2, 3, 4 })));
|
||||
System.out.println("...records=" + handler.records.size());
|
||||
System.out.println("keyLoggingUsesOnlySafeMetadataAndClearsPublicEncoding...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultFailureLoggingOmitsExceptionMessageAndThrowable() {
|
||||
System.out.println("defaultFailureLoggingOmitsExceptionMessageAndThrowable");
|
||||
RecordingHandler handler = new RecordingHandler();
|
||||
JulAuditListenerStd listener = listener(handler);
|
||||
|
||||
listener.onFailure("context", "read", "attach", new IllegalStateException(SENSITIVE_TEXT));
|
||||
|
||||
assertEquals(1, handler.records.size());
|
||||
assertFalse(handler.rendered().contains(SENSITIVE_TEXT));
|
||||
assertEquals(null, handler.records.get(0).getThrown());
|
||||
System.out.println("...failureRecords=1");
|
||||
System.out.println("defaultFailureLoggingOmitsExceptionMessageAndThrowable...ok");
|
||||
}
|
||||
|
||||
private static JulAuditListenerStd listener(RecordingHandler handler) {
|
||||
Logger logger = Logger.getLogger(
|
||||
JulAuditListenerStdSecurityTest.class.getName() + "." + LOGGER_IDS.incrementAndGet());
|
||||
logger.setUseParentHandlers(false);
|
||||
logger.setLevel(Level.ALL);
|
||||
handler.setLevel(Level.ALL);
|
||||
logger.addHandler(handler);
|
||||
return JulAuditListenerStd.builder().logger(logger).build();
|
||||
}
|
||||
|
||||
private static final class RecordingHandler extends Handler {
|
||||
private final List<LogRecord> records = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void publish(LogRecord record) {
|
||||
records.add(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
// No buffered state.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
records.clear();
|
||||
}
|
||||
|
||||
private String rendered() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (LogRecord record : records) {
|
||||
result.append(record.getMessage());
|
||||
if (record.getParameters() != null) {
|
||||
result.append(Arrays.toString(record.getParameters()));
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CountingSecretKey implements SecretKey {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private int encodedCalls;
|
||||
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return "CONTROLLED-SECRET";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return "RAW";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
encodedCalls++;
|
||||
return SENSITIVE_TEXT.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CountingPrivateKey implements PrivateKey {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private int encodedCalls;
|
||||
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return "CONTROLLED-PRIVATE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return "PKCS#8";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
encodedCalls++;
|
||||
return SENSITIVE_TEXT.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ControlledPublicKey implements PublicKey {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final byte[] encoding;
|
||||
|
||||
private ControlledPublicKey(byte[] encoding) {
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return "CONTROLLED-PUBLIC";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return "X.509";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
return encoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.io;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CipherTransformInputStreamBuilderTest {
|
||||
@Test
|
||||
void validatesEveryGeometryBoundaryWithoutAssertions() {
|
||||
System.out.print("ChunkTransform/geometry...");
|
||||
assertThrows(IllegalArgumentException.class, () -> new TestStream(1, 1, 1, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new TestStream(0, 1, 1, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new TestStream(2, 0, 1, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new TestStream(2, 1, 0, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new TestStream(2, 1, 1, -1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new TestStream(Integer.MAX_VALUE, 1, 2, 0));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new TestStream(2, 1, Integer.MAX_VALUE, 1));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new TestStream(2, Integer.MAX_VALUE, 1, 1));
|
||||
|
||||
new TestStream(2, 1, 1, 0);
|
||||
new TestStream(2, 1, 1, 1);
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidTransformOutputCounts() {
|
||||
System.out.print("ChunkTransform/counts...");
|
||||
TestStream negative = new TestStream(2, 2, 1, 0);
|
||||
negative.transformCount = -1;
|
||||
assertThrows(IllegalStateException.class, negative::read);
|
||||
|
||||
TestStream excessive = new TestStream(2, 2, 1, 0);
|
||||
excessive.transformCount = 3;
|
||||
assertThrows(IllegalStateException.class, excessive::read);
|
||||
|
||||
TestStream finalExcessive = new TestStream(2, 2, 1, 0, new byte[0]);
|
||||
finalExcessive.finalCount = 3;
|
||||
assertThrows(IllegalStateException.class, finalExcessive::read);
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAesIndependentBlocksBeforeReadingUpstream() throws Exception {
|
||||
System.out.print("CipherBuilder/algorithm-guard...");
|
||||
CountingInputStream gcmInput = new CountingInputStream();
|
||||
Cipher gcm = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> CipherTransformInputStreamBuilder.builder().withUpstream(gcmInput).withCipher(gcm)
|
||||
.withIndependentBlocks().build());
|
||||
assertEquals(0, gcmInput.reads);
|
||||
|
||||
CountingInputStream cbcInput = new CountingInputStream();
|
||||
Cipher cbc = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> CipherTransformInputStreamBuilder.builder().withUpstream(cbcInput).withCipher(cbc)
|
||||
.withLeftZeroPadding(true).withIndependentBlocks().build());
|
||||
assertEquals(0, cbcInput.reads);
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
private static final class TestStream extends AbstractChunkTransformInputStream {
|
||||
private int transformCount;
|
||||
private int finalCount;
|
||||
|
||||
private TestStream(int inputChunk, int outputChunk, int chunks, int finalChunks) {
|
||||
this(inputChunk, outputChunk, chunks, finalChunks, new byte[] { 1, 2 });
|
||||
}
|
||||
|
||||
private TestStream(int inputChunk, int outputChunk, int chunks, int finalChunks, byte[] input) {
|
||||
super(new ByteArrayInputStream(input), inputChunk, outputChunk, chunks, finalChunks);
|
||||
transformCount = outputChunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int transform(byte[] input, int inputOffset, int inputChunks, byte[] output) {
|
||||
return transformCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int doFinal(byte[] input, int inputOffset, int length, byte[] output, int outputOffset) {
|
||||
return finalCount;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CountingInputStream extends InputStream {
|
||||
private int reads;
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
reads++;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
383
lib/src/test/java/zeroecho/core/marshal/PairSeqCodecTest.java
Normal file
383
lib/src/test/java/zeroecho/core/marshal/PairSeqCodecTest.java
Normal file
@@ -0,0 +1,383 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.marshal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Verifies cached accessor plans for {@link PairSeqCodec}.
|
||||
*/
|
||||
class PairSeqCodecTest {
|
||||
|
||||
@Test
|
||||
void marshalAndStaticFactoryUnmarshalSucceed() {
|
||||
System.out.println("marshalAndStaticFactoryUnmarshalSucceed");
|
||||
PairSeqCodec<FactoryValue> codec = new PairSeqCodec<>(FactoryValue.class);
|
||||
|
||||
PairSeq representation = codec.marshal(new FactoryValue("alpha"));
|
||||
FactoryValue decoded = codec.unmarshal(representation);
|
||||
|
||||
assertEquals("alpha", representation.valAt(0));
|
||||
assertEquals("alpha", decoded.value);
|
||||
System.out.println("...decoded=" + decoded.value);
|
||||
System.out.println("marshalAndStaticFactoryUnmarshalSucceed...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedOperationsReuseCachedPlans() {
|
||||
System.out.println("repeatedOperationsReuseCachedPlans");
|
||||
PairSeqCodec<FactoryValue> codec = new PairSeqCodec<>(FactoryValue.class);
|
||||
Object marshalPlan = PairSeqCodec.cachedMarshalPlan(FactoryValue.class);
|
||||
Object unmarshalPlan = PairSeqCodec.cachedUnmarshalPlan(FactoryValue.class);
|
||||
|
||||
for (int index = 0; index < 20; index++) {
|
||||
PairSeq representation = codec.marshal(new FactoryValue(Integer.toString(index)));
|
||||
assertEquals(Integer.toString(index), codec.unmarshal(representation).value);
|
||||
}
|
||||
assertSame(marshalPlan, PairSeqCodec.cachedMarshalPlan(FactoryValue.class));
|
||||
assertSame(unmarshalPlan, PairSeqCodec.cachedUnmarshalPlan(FactoryValue.class));
|
||||
assertEquals(1, PairSeqCodec.marshalResolutionCount(FactoryValue.class));
|
||||
assertEquals(1, PairSeqCodec.unmarshalResolutionCount(FactoryValue.class));
|
||||
|
||||
System.out.println("...iterations=20");
|
||||
System.out.println("repeatedOperationsReuseCachedPlans...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void separateRuntimeClassesUseSeparateMarshalPlans() {
|
||||
System.out.println("separateRuntimeClassesUseSeparateMarshalPlans");
|
||||
Object first = PairSeqCodec.cachedMarshalPlan(FactoryValue.class);
|
||||
Object second = PairSeqCodec.cachedMarshalPlan(ConstructorValue.class);
|
||||
Object firstUnmarshal = PairSeqCodec.cachedUnmarshalPlan(FactoryValue.class);
|
||||
Object secondUnmarshal = PairSeqCodec.cachedUnmarshalPlan(ConstructorValue.class);
|
||||
|
||||
assertNotSame(first, second);
|
||||
assertNotSame(firstUnmarshal, secondUnmarshal);
|
||||
|
||||
System.out.println("...separatePlans=true");
|
||||
System.out.println("separateRuntimeClassesUseSeparateMarshalPlans...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorFallbackUnmarshalsSuccessfully() {
|
||||
System.out.println("constructorFallbackUnmarshalsSuccessfully");
|
||||
PairSeqCodec<ConstructorValue> codec = new PairSeqCodec<>(ConstructorValue.class);
|
||||
|
||||
ConstructorValue decoded = codec.unmarshal(PairSeq.of("value", "beta"));
|
||||
|
||||
assertEquals("beta", decoded.value);
|
||||
System.out.println("...decoded=" + decoded.value);
|
||||
System.out.println("constructorFallbackUnmarshalsSuccessfully...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void broadlyDeclaredFactoryReturnRemainsCompatible() {
|
||||
System.out.println("broadlyDeclaredFactoryReturnRemainsCompatible");
|
||||
PairSeqCodec<BroadFactoryValue> codec = new PairSeqCodec<>(BroadFactoryValue.class);
|
||||
|
||||
BroadFactoryValue decoded = codec.unmarshal(PairSeq.of("value", "broad"));
|
||||
|
||||
assertEquals("broad", decoded.value);
|
||||
System.out.println("...decoded=" + decoded.value);
|
||||
System.out.println("broadlyDeclaredFactoryReturnRemainsCompatible...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingAndIncompatibleMethodsReportContext() {
|
||||
System.out.println("missingAndIncompatibleMethodsReportContext");
|
||||
PairSeqCodec<MissingValue> missingCodec = new PairSeqCodec<>(MissingValue.class);
|
||||
PairSeqCodec<WrongMarshalValue> wrongMarshalCodec = new PairSeqCodec<>(WrongMarshalValue.class);
|
||||
PairSeqCodec<WrongFactoryValue> wrongFactoryCodec = new PairSeqCodec<>(WrongFactoryValue.class);
|
||||
|
||||
IllegalStateException missingMarshal = assertThrows(IllegalStateException.class,
|
||||
() -> missingCodec.marshal(new MissingValue()));
|
||||
IllegalStateException missingUnmarshal = assertThrows(IllegalStateException.class,
|
||||
() -> missingCodec.unmarshal(PairSeq.of()));
|
||||
IllegalStateException wrongMarshal = assertThrows(IllegalStateException.class,
|
||||
() -> wrongMarshalCodec.marshal(new WrongMarshalValue()));
|
||||
IllegalStateException wrongFactory = assertThrows(IllegalStateException.class,
|
||||
() -> wrongFactoryCodec.unmarshal(PairSeq.of()));
|
||||
|
||||
assertTrue(missingMarshal.getMessage().contains("marshal"));
|
||||
assertTrue(missingUnmarshal.getMessage().contains("unmarshal"));
|
||||
assertTrue(wrongMarshal.getMessage().contains("must return PairSeq"));
|
||||
assertTrue(wrongFactory.getMessage().contains("must return"));
|
||||
System.out.println("...negativeCases=4");
|
||||
System.out.println("missingAndIncompatibleMethodsReportContext...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetFailuresRetainInvocationContext() {
|
||||
System.out.println("targetFailuresRetainInvocationContext");
|
||||
PairSeqCodec<ThrowingValue> codec = new PairSeqCodec<>(ThrowingValue.class);
|
||||
|
||||
IllegalStateException marshalFailure = assertThrows(IllegalStateException.class,
|
||||
() -> codec.marshal(new ThrowingValue()));
|
||||
IllegalStateException unmarshalFailure = assertThrows(IllegalStateException.class,
|
||||
() -> codec.unmarshal(PairSeq.of()));
|
||||
|
||||
assertTrue(marshalFailure.getMessage().contains("marshal() failed"));
|
||||
assertTrue(unmarshalFailure.getMessage().contains("unmarshal(PairSeq) failed"));
|
||||
assertTrue(marshalFailure.getCause() instanceof java.lang.reflect.InvocationTargetException);
|
||||
assertTrue(unmarshalFailure.getCause() instanceof java.lang.reflect.InvocationTargetException);
|
||||
System.out.println("...causesPreserved=true");
|
||||
System.out.println("targetFailuresRetainInvocationContext...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullInputsAreRejected() {
|
||||
System.out.println("nullInputsAreRejected");
|
||||
PairSeqCodec<FactoryValue> codec = new PairSeqCodec<>(FactoryValue.class);
|
||||
|
||||
assertThrows(NullPointerException.class, () -> new PairSeqCodec<FactoryValue>(null));
|
||||
assertThrows(NullPointerException.class, () -> codec.marshal(null));
|
||||
assertThrows(NullPointerException.class, () -> codec.unmarshal(null));
|
||||
|
||||
System.out.println("...nullCases=3");
|
||||
System.out.println("nullInputsAreRejected...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentFirstAccessUsesOneCachedPlan() throws Exception {
|
||||
System.out.println("concurrentFirstAccessUsesOneCachedPlan");
|
||||
int taskCount = 24;
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
List<Future<PlanPair>> futures = new ArrayList<>();
|
||||
for (int index = 0; index < taskCount; index++) {
|
||||
futures.add(executor.submit(() -> {
|
||||
start.await();
|
||||
PairSeqCodec<ConcurrentValue> codec = new PairSeqCodec<>(ConcurrentValue.class);
|
||||
ConcurrentValue value = codec.unmarshal(codec.marshal(new ConcurrentValue("gamma")));
|
||||
assertEquals("gamma", value.value);
|
||||
return new PlanPair(PairSeqCodec.cachedMarshalPlan(ConcurrentValue.class),
|
||||
PairSeqCodec.cachedUnmarshalPlan(ConcurrentValue.class));
|
||||
}));
|
||||
}
|
||||
start.countDown();
|
||||
PlanPair expectedPlans = futures.get(0).get();
|
||||
for (Future<PlanPair> future : futures) {
|
||||
PlanPair actualPlans = future.get();
|
||||
assertSame(expectedPlans.marshal(), actualPlans.marshal());
|
||||
assertSame(expectedPlans.unmarshal(), actualPlans.unmarshal());
|
||||
}
|
||||
assertEquals(1, PairSeqCodec.marshalResolutionCount(ConcurrentValue.class));
|
||||
assertEquals(1, PairSeqCodec.unmarshalResolutionCount(ConcurrentValue.class));
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
System.out.println("...completedTasks=" + taskCount);
|
||||
System.out.println("concurrentFirstAccessUsesOneCachedPlan...ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test value exposing marshal and static-factory conventions.
|
||||
*/
|
||||
public static final class FactoryValue {
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* Creates a test value.
|
||||
*
|
||||
* @param value stored value
|
||||
*/
|
||||
public FactoryValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals this test value.
|
||||
*
|
||||
* @return pair representation
|
||||
*/
|
||||
public PairSeq marshal() {
|
||||
return PairSeq.of("value", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a test value.
|
||||
*
|
||||
* @param representation pair representation
|
||||
* @return reconstructed value
|
||||
*/
|
||||
public static FactoryValue unmarshal(PairSeq representation) {
|
||||
return new FactoryValue(representation.valAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test value exposing marshal and constructor conventions.
|
||||
*/
|
||||
public static final class ConstructorValue {
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* Creates a test value.
|
||||
*
|
||||
* @param value stored value
|
||||
*/
|
||||
public ConstructorValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a test value.
|
||||
*
|
||||
* @param representation pair representation
|
||||
*/
|
||||
public ConstructorValue(PairSeq representation) {
|
||||
this(representation.valAt(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals this test value.
|
||||
*
|
||||
* @return pair representation
|
||||
*/
|
||||
public PairSeq marshal() {
|
||||
return PairSeq.of("value", value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test value with no codec conventions.
|
||||
*/
|
||||
public static final class MissingValue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test value with an incompatible marshal return.
|
||||
*/
|
||||
public static final class WrongMarshalValue {
|
||||
/**
|
||||
* Returns an intentionally incompatible representation.
|
||||
*
|
||||
* @return incompatible value
|
||||
*/
|
||||
public String marshal() {
|
||||
return "wrong";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test value with an incompatible factory return.
|
||||
*/
|
||||
public static final class WrongFactoryValue {
|
||||
/**
|
||||
* Returns an intentionally incompatible value.
|
||||
*
|
||||
* @param representation ignored representation
|
||||
* @return incompatible value
|
||||
*/
|
||||
public static String unmarshal(PairSeq representation) {
|
||||
return representation.valAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test value whose codec operations fail.
|
||||
*/
|
||||
public static final class ThrowingValue {
|
||||
/**
|
||||
* Fails during marshalling.
|
||||
*
|
||||
* @return no value
|
||||
* @throws IllegalArgumentException always
|
||||
*/
|
||||
public PairSeq marshal() {
|
||||
throw new IllegalArgumentException("marshal target");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fails during unmarshalling.
|
||||
*
|
||||
* @param representation ignored representation
|
||||
* @return no value
|
||||
* @throws IllegalArgumentException always
|
||||
*/
|
||||
public static ThrowingValue unmarshal(PairSeq representation) {
|
||||
throw new IllegalArgumentException("unmarshal target");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test value whose factory declares a compatible broad return type.
|
||||
*/
|
||||
public static final class BroadFactoryValue {
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* Creates a test value.
|
||||
*
|
||||
* @param value stored value
|
||||
*/
|
||||
public BroadFactoryValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a test value through a broadly declared return type.
|
||||
*
|
||||
* @param representation pair representation
|
||||
* @return reconstructed value as {@link Object}
|
||||
*/
|
||||
public static Object unmarshal(PairSeq representation) {
|
||||
return new BroadFactoryValue(representation.valAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test value reserved for concurrent cold-cache access.
|
||||
*/
|
||||
public static final class ConcurrentValue {
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* Creates a test value.
|
||||
*
|
||||
* @param value stored value
|
||||
*/
|
||||
public ConcurrentValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals this test value.
|
||||
*
|
||||
* @return pair representation
|
||||
*/
|
||||
public PairSeq marshal() {
|
||||
return PairSeq.of("value", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a test value.
|
||||
*
|
||||
* @param representation pair representation
|
||||
* @return reconstructed value
|
||||
*/
|
||||
public static ConcurrentValue unmarshal(PairSeq representation) {
|
||||
return new ConcurrentValue(representation.valAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
private record PlanPair(Object marshal, Object unmarshal) {
|
||||
}
|
||||
}
|
||||
138
lib/src/test/java/zeroecho/core/marshal/PairSeqTest.java
Normal file
138
lib/src/test/java/zeroecho/core/marshal/PairSeqTest.java
Normal file
@@ -0,0 +1,138 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.marshal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PairSeqTest {
|
||||
@Test
|
||||
void checkedRoundTrip() throws Exception {
|
||||
System.out.println("checkedRoundTrip");
|
||||
PairSeq empty = PairSeq.of();
|
||||
assertEquals(0, empty.size());
|
||||
|
||||
PairSeq original = PairSeq.of("a", "1", "b", "");
|
||||
StringBuilder output = new StringBuilder();
|
||||
original.writeTo(output);
|
||||
PairSeq decoded = PairSeq.readFrom(new StringReader(output.toString()));
|
||||
|
||||
assertEquals(2, decoded.size());
|
||||
assertEquals("a", decoded.keyAt(0));
|
||||
assertEquals("1", decoded.valAt(0));
|
||||
assertEquals("", decoded.valAt(1));
|
||||
System.out.println("...pairs=" + decoded.size());
|
||||
System.out.println("checkedRoundTrip...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationAndOwnership() {
|
||||
System.out.println("validationAndOwnership");
|
||||
assertThrows(IllegalArgumentException.class, () -> PairSeq.of((String[]) null));
|
||||
assertThrows(IllegalArgumentException.class, () -> PairSeq.of("key"));
|
||||
assertEquals("pair 0 key must not be null",
|
||||
assertThrows(IllegalArgumentException.class, () -> PairSeq.of(null, "value")).getMessage());
|
||||
assertEquals("pair 0 value must not be null",
|
||||
assertThrows(IllegalArgumentException.class, () -> PairSeq.of("key", null)).getMessage());
|
||||
assertEquals("pair 1 key must not be null",
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> PairSeq.of("first", "value", null, "other")).getMessage());
|
||||
|
||||
String[] source = { "key", "value" };
|
||||
PairSeq sequence = PairSeq.of(source);
|
||||
source[0] = "changed";
|
||||
assertEquals("key", sequence.keyAt(0));
|
||||
assertNotSame(source, sequence);
|
||||
System.out.println("...owned=true");
|
||||
System.out.println("validationAndOwnership...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failurePropagation() {
|
||||
System.out.println("failurePropagation");
|
||||
IOException immediate = new IOException("immediate");
|
||||
IOException checked = assertThrows(IOException.class,
|
||||
() -> PairSeq.of("a", "b").writeTo(new FailingAppendable(0, immediate)));
|
||||
assertSame(immediate, checked);
|
||||
|
||||
IOException partial = new IOException("partial");
|
||||
FailingAppendable partialOutput = new FailingAppendable(2, partial);
|
||||
IOException partialActual = assertThrows(IOException.class,
|
||||
() -> PairSeq.of("a", "b").writeTo(partialOutput));
|
||||
assertSame(partial, partialActual);
|
||||
assertEquals("a=", partialOutput.output.toString());
|
||||
|
||||
IllegalStateException runtime = new IllegalStateException("runtime");
|
||||
IllegalStateException runtimeActual = assertThrows(IllegalStateException.class,
|
||||
() -> PairSeq.of("a", "b").writeTo(new RuntimeFailingAppendable(runtime)));
|
||||
assertSame(runtime, runtimeActual);
|
||||
System.out.println("...partialChars=" + partialOutput.output.length());
|
||||
System.out.println("failurePropagation...ok");
|
||||
}
|
||||
|
||||
private static final class FailingAppendable implements Appendable {
|
||||
private final int acceptedCharacters;
|
||||
private final IOException failure;
|
||||
private final StringBuilder output = new StringBuilder();
|
||||
|
||||
private FailingAppendable(int acceptedCharacters, IOException failure) {
|
||||
this.acceptedCharacters = acceptedCharacters;
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Appendable append(CharSequence sequence) throws IOException {
|
||||
for (int index = 0; index < sequence.length(); index++) {
|
||||
append(sequence.charAt(index));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Appendable append(CharSequence sequence, int start, int end) throws IOException {
|
||||
return append(sequence.subSequence(start, end));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Appendable append(char character) throws IOException {
|
||||
if (output.length() == acceptedCharacters) {
|
||||
throw failure;
|
||||
}
|
||||
output.append(character);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RuntimeFailingAppendable implements Appendable {
|
||||
private final IllegalStateException failure;
|
||||
|
||||
private RuntimeFailingAppendable(IllegalStateException failure) {
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Appendable append(CharSequence sequence) {
|
||||
throw failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Appendable append(CharSequence sequence, int start, int end) {
|
||||
throw failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Appendable append(char character) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,8 +62,10 @@ 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.core.KeyOperation;
|
||||
import zeroecho.core.KeyOperationInfo;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.SymmetricKeyGenerator;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
public class KeyringStoreDynamicTest {
|
||||
@@ -183,19 +185,19 @@ public class KeyringStoreDynamicTest {
|
||||
logBegin();
|
||||
|
||||
Path keyringPath = tempDir.resolve("keyring-" + System.nanoTime() + ".txt");
|
||||
KeyringStore store = new KeyringStore();
|
||||
KeyringStore store = new KeyringStore(new zeroecho.sdk.ZeroEchoSession());
|
||||
|
||||
CryptoAlgorithm algo = CryptoAlgorithms.require("RSA");
|
||||
KeyPair kp = algo.generateKeyPair(RsaKeyGenSpec.rsa4096());
|
||||
zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession();
|
||||
KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("RSA", 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());
|
||||
kp = session.keyBuilders().asymmetric().generateKeyPair("RSA", 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);
|
||||
store = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), keyringPath);
|
||||
String s = store.exportText(Collections.singleton("alice.pub"));
|
||||
|
||||
assertTrue(s.contains("# KeyringStore v1\n"));
|
||||
@@ -214,7 +216,7 @@ public class KeyringStoreDynamicTest {
|
||||
void keyring_dynamic_population_roundtrip_and_dump(@TempDir Path tempDir) throws Exception {
|
||||
logBegin();
|
||||
|
||||
KeyringStore store = new KeyringStore();
|
||||
KeyringStore store = new KeyringStore(new zeroecho.sdk.ZeroEchoSession());
|
||||
|
||||
Set<String> ids = CryptoAlgorithms.available();
|
||||
System.out.println("...algorithms discovered: " + ids);
|
||||
@@ -225,34 +227,35 @@ public class KeyringStoreDynamicTest {
|
||||
CryptoAlgorithm alg = CryptoAlgorithms.require(id);
|
||||
System.out.println("\n-- " + id + " --");
|
||||
|
||||
if (!alg.asymmetricBuildersInfo().isEmpty()) {
|
||||
if (alg.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE)) {
|
||||
int perAlg = 0;
|
||||
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
|
||||
if (bi.defaultKeySpec == null) {
|
||||
for (KeyOperationInfo bi : alg.keyOperations()) {
|
||||
if (bi.operation() != KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE || bi.defaultSpec() == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> genSpecType = (Class<AlgorithmKeySpec>) bi.specType;
|
||||
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(genSpecType);
|
||||
Class<AlgorithmKeySpec> genSpecType = (Class<AlgorithmKeySpec>) bi.specType();
|
||||
AsymmetricKeyPairGenerator<AlgorithmKeySpec> b = alg.asymmetricKeyPairGenerator(genSpecType);
|
||||
|
||||
AlgorithmKeySpec genSpec = (AlgorithmKeySpec) bi.defaultKeySpec;
|
||||
AlgorithmKeySpec genSpec = bi.defaultSpec();
|
||||
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;
|
||||
for (KeyOperationInfo x : alg.keyOperations()) {
|
||||
if (x.operation() == KeyOperation.ASYMMETRIC_PUBLIC_IMPORT) {
|
||||
pubImpType = x.specType();
|
||||
} else if (x.operation() == KeyOperation.ASYMMETRIC_PRIVATE_IMPORT) {
|
||||
prvImpType = x.specType();
|
||||
}
|
||||
}
|
||||
if (pubImpType != null) {
|
||||
AlgorithmKeySpec pubSpec = makeImportSpec(pubImpType, pub.getEncoded(), id,
|
||||
bi.defaultKeySpec);
|
||||
bi.defaultSpec());
|
||||
String alias = id.toLowerCase() + "-pub-" + perAlg;
|
||||
store.putPublic(alias, id, pubSpec);
|
||||
System.out.println("..." + alias + " saved, len=" + encLen(pub.getEncoded()));
|
||||
@@ -262,7 +265,7 @@ public class KeyringStoreDynamicTest {
|
||||
}
|
||||
if (prvImpType != null) {
|
||||
AlgorithmKeySpec prvSpec = makeImportSpec(prvImpType, prv.getEncoded(), id,
|
||||
bi.defaultKeySpec);
|
||||
bi.defaultSpec());
|
||||
String alias = id.toLowerCase() + "-prv-" + perAlg;
|
||||
store.putPrivate(alias, id, prvSpec);
|
||||
System.out.println("..." + alias + " saved, len=" + encLen(prv.getEncoded()));
|
||||
@@ -282,46 +285,37 @@ public class KeyringStoreDynamicTest {
|
||||
}
|
||||
}
|
||||
|
||||
if (!alg.symmetricBuildersInfo().isEmpty()) {
|
||||
if (alg.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE)) {
|
||||
int perAlg = 0;
|
||||
for (CryptoAlgorithm.SymBuilderInfo bi : alg.symmetricBuildersInfo()) {
|
||||
if (bi.defaultKeySpec() == null) {
|
||||
for (KeyOperationInfo bi : alg.keyOperations()) {
|
||||
if (bi.operation() != KeyOperation.SYMMETRIC_GENERATE || bi.defaultSpec() == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> genSpecType = (Class<AlgorithmKeySpec>) bi.specType();
|
||||
SymmetricKeyBuilder<AlgorithmKeySpec> b = alg.symmetricKeyBuilder(genSpecType);
|
||||
SymmetricKeyGenerator<AlgorithmKeySpec> b = alg.symmetricKeyGenerator(genSpecType);
|
||||
|
||||
AlgorithmKeySpec genSpec = (AlgorithmKeySpec) bi.defaultKeySpec();
|
||||
AlgorithmKeySpec genSpec = bi.defaultSpec();
|
||||
SecretKey sk = b.generateSecret(genSpec);
|
||||
|
||||
Class<?> impType = null;
|
||||
for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) {
|
||||
if (looksLikeImportSpecForSecret(x.specType())) {
|
||||
for (KeyOperationInfo x : alg.keyOperations()) {
|
||||
if (x.operation() == KeyOperation.SYMMETRIC_IMPORT
|
||||
&& 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());
|
||||
AlgorithmKeySpec imp = makeImportSpec(impType, raw, id, bi.defaultSpec());
|
||||
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));
|
||||
System.out.println("..." + alias + " saved, len=" + raw.length);
|
||||
totalAdded++;
|
||||
} else {
|
||||
System.out.println("...*** SKIP *** no symmetric import spec for " + id);
|
||||
@@ -344,7 +338,7 @@ public class KeyringStoreDynamicTest {
|
||||
System.out.println("\n...saved keyring: " + keyringPath.getFileName());
|
||||
System.out.println("...entries stored: " + totalAdded);
|
||||
|
||||
KeyringStore loaded = KeyringStore.load(keyringPath);
|
||||
KeyringStore loaded = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), keyringPath);
|
||||
assertTrue(loaded.aliases().size() >= Math.min(totalAdded, 1), "no entries reloaded");
|
||||
|
||||
int ok = 0;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
|
||||
class KeyringStoreSecurityTest {
|
||||
private static final AtomicBoolean UNREGISTERED_INITIALIZED = new AtomicBoolean();
|
||||
|
||||
@Test
|
||||
void rejectsUnregisteredPersistedSpecBeforeClassInitialization() throws Exception {
|
||||
System.out.println("rejectsUnregisteredPersistedSpecBeforeClassInitialization");
|
||||
KeyringStore store = new KeyringStore(new ZeroEchoSession());
|
||||
String text = "# KeyringStore v1\n"
|
||||
+ "@entry\n"
|
||||
+ "alias=attacker.pub\n"
|
||||
+ "algorithm=RSA\n"
|
||||
+ "kind=PUBLIC_KEY\n"
|
||||
+ "spec=zeroecho.core.storage.KeyringStoreSecurityTest$UnregisteredSpec\n\n";
|
||||
store.importText(text, false);
|
||||
|
||||
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
|
||||
() -> store.getPublic("attacker"));
|
||||
|
||||
assertFalse(UNREGISTERED_INITIALIZED.get());
|
||||
assertTrue(failure.getMessage().contains("not registered"));
|
||||
System.out.println("...classInitialized=false");
|
||||
System.out.println("rejectsUnregisteredPersistedSpecBeforeClassInitialization...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSpecRegisteredForDifferentOperation() throws Exception {
|
||||
System.out.println("rejectsSpecRegisteredForDifferentOperation");
|
||||
KeyringStore store = new KeyringStore(new ZeroEchoSession());
|
||||
String text = "# KeyringStore v1\n"
|
||||
+ "@entry\n"
|
||||
+ "alias=mismatch.pub\n"
|
||||
+ "algorithm=RSA\n"
|
||||
+ "kind=PUBLIC_KEY\n"
|
||||
+ "spec=zeroecho.core.alg.rsa.RsaPrivateKeySpec\n\n";
|
||||
store.importText(text, false);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> store.getPublic("mismatch"));
|
||||
System.out.println("...mismatchedOperationRejected=true");
|
||||
System.out.println("rejectsSpecRegisteredForDifferentOperation...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void temporarySpecDestructionIsIdempotentAndObservable() throws Exception {
|
||||
System.out.println("temporarySpecDestructionIsIdempotentAndObservable");
|
||||
ControlledDestroyableSpec spec = new ControlledDestroyableSpec(false);
|
||||
|
||||
KeyringStore.destroyTemporarySpec(spec, null);
|
||||
KeyringStore.destroyTemporarySpec(spec, null);
|
||||
|
||||
assertTrue(spec.isDestroyed());
|
||||
assertEquals(1, spec.destroyCalls);
|
||||
System.out.println("...destroyCalls=" + spec.destroyCalls);
|
||||
System.out.println("temporarySpecDestructionIsIdempotentAndObservable...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void destructionFailureIsSuppressedOnPrimaryFailure() throws Exception {
|
||||
System.out.println("destructionFailureIsSuppressedOnPrimaryFailure");
|
||||
ControlledDestroyableSpec spec = new ControlledDestroyableSpec(true);
|
||||
IllegalStateException primary = new IllegalStateException("controlled primary");
|
||||
|
||||
KeyringStore.destroyTemporarySpec(spec, primary);
|
||||
|
||||
assertEquals(1, primary.getSuppressed().length);
|
||||
assertTrue(primary.getSuppressed()[0] instanceof DestroyFailedException);
|
||||
System.out.println("...suppressedFailures=1");
|
||||
System.out.println("destructionFailureIsSuppressedOnPrimaryFailure...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void destructionFailureWithoutPrimaryUsesSecurityExceptionFamily() {
|
||||
System.out.println("destructionFailureWithoutPrimaryUsesSecurityExceptionFamily");
|
||||
ControlledDestroyableSpec spec = new ControlledDestroyableSpec(true);
|
||||
|
||||
GeneralSecurityException failure = assertThrows(GeneralSecurityException.class,
|
||||
() -> KeyringStore.destroyTemporarySpec(spec, null));
|
||||
|
||||
assertSame(DestroyFailedException.class, failure.getCause().getClass());
|
||||
System.out.println("...failureType=" + failure.getClass().getSimpleName());
|
||||
System.out.println("destructionFailureWithoutPrimaryUsesSecurityExceptionFamily...ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* A deliberately unregistered type whose initialization must never occur.
|
||||
*/
|
||||
public static final class UnregisteredSpec implements AlgorithmKeySpec {
|
||||
static {
|
||||
UNREGISTERED_INITIALIZED.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ControlledDestroyableSpec implements AlgorithmKeySpec, Destroyable {
|
||||
private final boolean fail;
|
||||
private boolean destroyed;
|
||||
private int destroyCalls;
|
||||
|
||||
private ControlledDestroyableSpec(boolean fail) {
|
||||
this.fail = fail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws DestroyFailedException {
|
||||
destroyCalls++;
|
||||
if (fail) {
|
||||
throw new DestroyFailedException("controlled destruction failure");
|
||||
}
|
||||
destroyed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return destroyed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
|
||||
class ZeroEchoSessionDestroyKeyTest {
|
||||
@Test
|
||||
void strictDestroyDistinguishesEveryLifecycleOutcome() throws Exception {
|
||||
System.out.print("ZeroEchoSession/destroy-strict...");
|
||||
AtomicInteger audits = new AtomicInteger();
|
||||
ZeroEchoSession session = new ZeroEchoSession().withAuditListener(new AuditListener() {
|
||||
@Override
|
||||
public void onKeyDestroyed(String id, String provider, Key key) {
|
||||
audits.incrementAndGet();
|
||||
}
|
||||
});
|
||||
|
||||
TestKey success = new TestKey(Behavior.SUCCESS);
|
||||
assertTrue(session.destroyKey("test", "provider", success));
|
||||
assertFalse(session.destroyKey("test", "provider", success));
|
||||
assertEquals(1, audits.get());
|
||||
assertFalse(session.destroyKey("test", "provider", new PlainKey()));
|
||||
assertThrows(NullPointerException.class, () -> session.destroyKey("test", "provider", null));
|
||||
assertThrows(DestroyFailedException.class,
|
||||
() -> session.destroyKey("test", "provider", new TestKey(Behavior.FAIL_CHECKED)));
|
||||
assertThrows(DestroyFailedException.class,
|
||||
() -> session.destroyKey("test", "provider", new TestKey(Behavior.NO_TRANSITION)));
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> session.destroyKey("test", "provider", new TestKey(Behavior.FAIL_RUNTIME)));
|
||||
assertEquals(1, audits.get());
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentStrictDestroyReportsAndAuditsOneTransition() throws Exception {
|
||||
System.out.print("ZeroEchoSession/destroy-concurrent...");
|
||||
AtomicInteger audits = new AtomicInteger();
|
||||
ZeroEchoSession session = new ZeroEchoSession().withAuditListener(new AuditListener() {
|
||||
@Override
|
||||
public void onKeyDestroyed(String id, String provider, Key key) {
|
||||
audits.incrementAndGet();
|
||||
}
|
||||
});
|
||||
BlockingKey key = new BlockingKey();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<Boolean> first = executor.submit(() -> session.destroyKey("test", "provider", key));
|
||||
key.destroyEntered.await();
|
||||
Future<Boolean> second = executor.submit(() -> session.destroyKey("test", "provider", key));
|
||||
key.allowDestroy.countDown();
|
||||
|
||||
boolean firstResult = first.get();
|
||||
boolean secondResult = second.get();
|
||||
assertTrue(firstResult ^ secondResult);
|
||||
assertEquals(1, audits.get());
|
||||
assertEquals(1, key.destroyCalls.get());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
private enum Behavior {
|
||||
SUCCESS,
|
||||
FAIL_CHECKED,
|
||||
FAIL_RUNTIME,
|
||||
NO_TRANSITION
|
||||
}
|
||||
|
||||
private static final class TestKey implements Key, Destroyable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String SECRET = "secret-key-marker";
|
||||
private final Behavior behavior;
|
||||
private boolean destroyed;
|
||||
private boolean encodedCalled;
|
||||
private boolean toStringCalled;
|
||||
|
||||
private TestKey(Behavior behavior) {
|
||||
this.behavior = behavior;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws DestroyFailedException {
|
||||
switch (behavior) {
|
||||
case SUCCESS -> destroyed = true;
|
||||
case FAIL_CHECKED -> throw new DestroyFailedException(SECRET);
|
||||
case FAIL_RUNTIME -> throw new IllegalStateException(SECRET);
|
||||
case NO_TRANSITION -> {
|
||||
// Intentionally does not transition.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return destroyed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return "test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return "RAW";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
encodedCalled = true;
|
||||
return SECRET.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
toStringCalled = true;
|
||||
return SECRET;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PlainKey implements Key {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return "plain";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BlockingKey implements Key, Destroyable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final CountDownLatch destroyEntered = new CountDownLatch(1);
|
||||
private final CountDownLatch allowDestroy = new CountDownLatch(1);
|
||||
private final AtomicInteger destroyCalls = new AtomicInteger();
|
||||
private boolean destroyed;
|
||||
|
||||
@Override
|
||||
public void destroy() throws DestroyFailedException {
|
||||
destroyCalls.incrementAndGet();
|
||||
destroyEntered.countDown();
|
||||
try {
|
||||
allowDestroy.await();
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new DestroyFailedException("Interrupted while testing destruction");
|
||||
}
|
||||
destroyed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return destroyed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return "test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,15 +43,20 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
import zeroecho.core.audit.AuditMode;
|
||||
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
|
||||
import zeroecho.core.alg.xdh.XdhSpec;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexContext;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexContexts;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexPolicy;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexProfile;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexTranscript;
|
||||
@@ -87,20 +92,20 @@ class HybridKexBuilderTest {
|
||||
HybridKexTranscript transcript = new HybridKexTranscript().addUtf8("suite", "X25519+ML-KEM-768").addUtf8("role",
|
||||
"builder-test");
|
||||
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
HybridKexContext alice = null;
|
||||
HybridKexContext bob = null;
|
||||
|
||||
try {
|
||||
alice = HybridKexBuilder.builder().profile(profile).transcript(transcript).classicAgreement()
|
||||
alice = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcript).classicAgreement()
|
||||
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate())
|
||||
.peerPublic(bobClassic.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
|
||||
.buildInitiator();
|
||||
|
||||
bob = HybridKexBuilder.builder().profile(profile).transcript(transcript).classicAgreement().algorithm("Xdh")
|
||||
bob = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcript).classicAgreement().algorithm("Xdh")
|
||||
.spec(XdhSpec.X25519).privateKey(bobClassic.getPrivate()).peerPublic(aliceClassic.getPublic())
|
||||
.pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate()).buildResponder();
|
||||
|
||||
@@ -133,19 +138,19 @@ class HybridKexBuilderTest {
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
HybridKexContext alice = null;
|
||||
HybridKexContext bob = null;
|
||||
|
||||
try {
|
||||
alice = HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh")
|
||||
alice = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicPairMessage().algorithm("Xdh")
|
||||
.spec(XdhSpec.X25519).keyPair(new KeyPairKey(aliceClassic)).pqcKem().algorithm("ML-KEM")
|
||||
.peerPublic(bobPqc.getPublic()).buildInitiator();
|
||||
|
||||
bob = HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
bob = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
.keyPair(new KeyPairKey(bobClassic)).pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate())
|
||||
.buildResponder();
|
||||
|
||||
@@ -179,12 +184,12 @@ class HybridKexBuilderTest {
|
||||
void buildInitiatorWithoutProfileFails() throws Exception {
|
||||
System.out.println("buildInitiatorWithoutProfileFails");
|
||||
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||
HybridKexBuilder.builder().classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
.privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic()).pqcKem()
|
||||
.algorithm("ML-KEM").peerPublic(bobPqc.getPublic()).buildInitiator();
|
||||
});
|
||||
@@ -200,10 +205,10 @@ class HybridKexBuilderTest {
|
||||
System.out.println("buildInitiatorWithoutClassicModeFails");
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||
HybridKexBuilder.builder().profile(profile).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
|
||||
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
|
||||
.buildInitiator();
|
||||
});
|
||||
|
||||
@@ -218,11 +223,11 @@ class HybridKexBuilderTest {
|
||||
System.out.println("buildInitiatorClassicAgreementWithoutPeerPublicFails");
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||
HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
.privateKey(aliceClassic.getPrivate()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
|
||||
.buildInitiator();
|
||||
});
|
||||
@@ -238,10 +243,10 @@ class HybridKexBuilderTest {
|
||||
System.out.println("buildResponderPairMessageWithoutKeyPairFails");
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||
HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
.pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate()).buildResponder();
|
||||
});
|
||||
|
||||
@@ -256,11 +261,11 @@ class HybridKexBuilderTest {
|
||||
System.out.println("buildInitiatorWithoutPqcPeerPublicFails");
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||
HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
.privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic()).pqcKem()
|
||||
.algorithm("ML-KEM").buildInitiator();
|
||||
});
|
||||
@@ -276,11 +281,11 @@ class HybridKexBuilderTest {
|
||||
System.out.println("buildResponderWithoutPqcPrivateFails");
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||
HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
.privateKey(bobClassic.getPrivate()).peerPublic(aliceClassic.getPublic()).pqcKem()
|
||||
.algorithm("ML-KEM").buildResponder();
|
||||
});
|
||||
@@ -298,12 +303,12 @@ class HybridKexBuilderTest {
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(16);
|
||||
HybridKexPolicy policy = new HybridKexPolicy(0, 0, 32);
|
||||
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
|
||||
HybridKexBuilder.builder().profile(profile).policy(policy).classicAgreement().algorithm("Xdh")
|
||||
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).policy(policy).classicAgreement().algorithm("Xdh")
|
||||
.spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic())
|
||||
.pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic()).buildInitiator();
|
||||
});
|
||||
@@ -314,18 +319,111 @@ class HybridKexBuilderTest {
|
||||
System.out.println("buildInitiatorRejectsPolicyWhenOkmTooShort...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void policyFailureClosesBothConstructedLegs() throws Exception {
|
||||
System.out.println("policyFailureClosesBothConstructedLegs");
|
||||
AtomicInteger closedContexts = new AtomicInteger();
|
||||
AuditListener listener = new AuditListener() {
|
||||
@Override
|
||||
public void onContextClosed(String contextId, long bodyBytes, long trailerBytes, long durationMillis) {
|
||||
closedContexts.incrementAndGet();
|
||||
}
|
||||
};
|
||||
ZeroEchoSession session = new ZeroEchoSession()
|
||||
.withAuditListener(listener)
|
||||
.withAuditMode(AuditMode.WRAP);
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(16);
|
||||
HybridKexPolicy policy = new HybridKexPolicy(0, 0, 32);
|
||||
KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = session.keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
||||
KyberKeyGenSpec.kyber768());
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> HybridKexBuilder.builder(session).profile(profile).policy(policy).classicAgreement()
|
||||
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate())
|
||||
.peerPublic(bobClassic.getPublic()).pqcKem().algorithm("ML-KEM")
|
||||
.peerPublic(bobPqc.getPublic()).buildInitiator());
|
||||
|
||||
assertEquals(2, closedContexts.get());
|
||||
System.out.println("...closedContexts=" + closedContexts.get());
|
||||
System.out.println("policyFailureClosesBothConstructedLegs...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void secondLegPolicyFailureClosesFirstFactoryLeg() throws Exception {
|
||||
System.out.println("secondLegPolicyFailureClosesFirstFactoryLeg");
|
||||
AtomicInteger closedContexts = new AtomicInteger();
|
||||
AuditListener listener = new AuditListener() {
|
||||
@Override
|
||||
public void onContextClosed(String contextId, long bodyBytes, long trailerBytes, long durationMillis) {
|
||||
closedContexts.incrementAndGet();
|
||||
}
|
||||
};
|
||||
ZeroEchoSession keySession = new ZeroEchoSession();
|
||||
KeyPair aliceClassic = keySession.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = keySession.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = keySession.keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
||||
KyberKeyGenSpec.kyber768());
|
||||
ZeroEchoSession operationSession = new ZeroEchoSession()
|
||||
.withAuditListener(listener)
|
||||
.withAuditMode(AuditMode.WRAP)
|
||||
.withPolicy((id, role, key, spec) -> {
|
||||
if ("ML-KEM".equals(id)) {
|
||||
throw new IllegalArgumentException("controlled PQ policy denial");
|
||||
}
|
||||
});
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> HybridKexContexts.initiator(operationSession, HybridKexProfile.defaultProfile(32),
|
||||
"Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(), XdhSpec.X25519,
|
||||
"ML-KEM", bobPqc.getPublic(), null));
|
||||
|
||||
assertEquals(1, closedContexts.get());
|
||||
System.out.println("...closedContexts=" + closedContexts.get());
|
||||
System.out.println("secondLegPolicyFailureClosesFirstFactoryLeg...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidFactoryInputIsRejectedBeforeContextAllocation() throws Exception {
|
||||
System.out.println("invalidFactoryInputIsRejectedBeforeContextAllocation");
|
||||
AtomicInteger createdContexts = new AtomicInteger();
|
||||
AuditListener listener = new AuditListener() {
|
||||
@Override
|
||||
public void onContextCreatedMeta(String contextId, String algorithmId, String provider,
|
||||
zeroecho.core.KeyUsage role, String keyFingerprint,
|
||||
java.util.Map<String, Object> specMeta) {
|
||||
createdContexts.incrementAndGet();
|
||||
}
|
||||
};
|
||||
ZeroEchoSession session = new ZeroEchoSession()
|
||||
.withAuditListener(listener)
|
||||
.withAuditMode(AuditMode.WRAP);
|
||||
KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> HybridKexContexts.initiator(session, HybridKexProfile.defaultProfile(32),
|
||||
"Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(), XdhSpec.X25519,
|
||||
"ML-KEM", null, null));
|
||||
|
||||
assertEquals(0, createdContexts.get());
|
||||
System.out.println("...createdContexts=0");
|
||||
System.out.println("invalidFactoryInputIsRejectedBeforeContextAllocation...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void switchingClassicModeClearsConflictingStateAndBuildsPairMessage() throws Exception {
|
||||
System.out.println("switchingClassicModeClearsConflictingStateAndBuildsPairMessage");
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
KeyPair agreementKeyPair = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair pairMessageKeyPair = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair agreementKeyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair pairMessageKeyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
HybridKexContext context = null;
|
||||
try {
|
||||
HybridKexBuilder builder = HybridKexBuilder.builder().profile(profile);
|
||||
HybridKexBuilder builder = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile);
|
||||
|
||||
builder.classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519).privateKey(agreementKeyPair.getPrivate())
|
||||
.peerPublic(agreementKeyPair.getPublic());
|
||||
@@ -352,13 +450,13 @@ class HybridKexBuilderTest {
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
KeyPair aliceClassicA = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassicA = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqcA = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair aliceClassicA = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassicA = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqcA = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
KeyPair aliceClassicB = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassicB = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqcB = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair aliceClassicB = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassicB = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobPqcB = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
HybridKexTranscript transcriptA = new HybridKexTranscript().addUtf8("context", "A");
|
||||
HybridKexTranscript transcriptB = new HybridKexTranscript().addUtf8("context", "B");
|
||||
@@ -372,12 +470,12 @@ class HybridKexBuilderTest {
|
||||
HybridKexContext bobB = null;
|
||||
|
||||
try {
|
||||
aliceA = HybridKexBuilder.builder().profile(profile).transcript(transcriptA).classicAgreement()
|
||||
aliceA = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcriptA).classicAgreement()
|
||||
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassicA.getPrivate())
|
||||
.peerPublic(bobClassicA.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqcA.getPublic())
|
||||
.buildInitiator();
|
||||
|
||||
bobA = HybridKexBuilder.builder().profile(profile).transcript(transcriptA).classicAgreement()
|
||||
bobA = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcriptA).classicAgreement()
|
||||
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(bobClassicA.getPrivate())
|
||||
.peerPublic(aliceClassicA.getPublic()).pqcKem().algorithm("ML-KEM").privateKey(bobPqcA.getPrivate())
|
||||
.buildResponder();
|
||||
@@ -389,12 +487,12 @@ class HybridKexBuilderTest {
|
||||
System.out.println("...responderA=" + hex(responderA));
|
||||
assertArrayEquals(secretA, responderA);
|
||||
|
||||
aliceB = HybridKexBuilder.builder().profile(profile).transcript(transcriptB).classicAgreement()
|
||||
aliceB = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcriptB).classicAgreement()
|
||||
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassicB.getPrivate())
|
||||
.peerPublic(bobClassicB.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqcB.getPublic())
|
||||
.buildInitiator();
|
||||
|
||||
bobB = HybridKexBuilder.builder().profile(profile).transcript(transcriptB).classicAgreement()
|
||||
bobB = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcriptB).classicAgreement()
|
||||
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(bobClassicB.getPrivate())
|
||||
.peerPublic(aliceClassicB.getPublic()).pqcKem().algorithm("ML-KEM").privateKey(bobPqcB.getPrivate())
|
||||
.buildResponder();
|
||||
@@ -468,4 +566,4 @@ class HybridKexBuilderTest {
|
||||
return "classicLen=?, pqcLen=?";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,10 @@ 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.KeyOperation;
|
||||
import zeroecho.core.KeyOperationInfo;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.SymmetricKeyGenerator;
|
||||
import zeroecho.core.tag.TagEngine;
|
||||
import zeroecho.core.tag.TagEngineBuilder;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
@@ -173,26 +175,26 @@ public class TagTrailerDataContentBuilderTest {
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
// AES key
|
||||
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
|
||||
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyGenerator(AesKeyGenSpec.class)
|
||||
.generateSecret(AesKeyGenSpec.aes256());
|
||||
|
||||
// Ed25519 keys (JCA)
|
||||
KeyPair ed = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
|
||||
KeyPair ed = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
|
||||
|
||||
TagEngine<Signature> tagEnc = TagEngineBuilder.ed25519Sign(ed.getPrivate()).get();
|
||||
TagEngine<Signature> tagDec = TagEngineBuilder.ed25519Verify(ed.getPublic()).get();
|
||||
TagEngine<Signature> tagEnc = TagEngineBuilder.ed25519Sign(new zeroecho.sdk.ZeroEchoSession(), ed.getPrivate()).get();
|
||||
TagEngine<Signature> tagDec = TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), 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();
|
||||
.add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader())
|
||||
.add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build();
|
||||
|
||||
byte[] pt = readAll(dec.getStream());
|
||||
@@ -210,28 +212,28 @@ public class TagTrailerDataContentBuilderTest {
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
// AES key
|
||||
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
|
||||
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyGenerator(AesKeyGenSpec.class)
|
||||
.generateSecret(AesKeyGenSpec.aes256());
|
||||
|
||||
// SPHINCS+ key pair via registry (uses default param set from
|
||||
// SphincsPlusKeyGenSpec)
|
||||
KeyPair spx = CryptoAlgorithms.keyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec());
|
||||
KeyPair spx = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec());
|
||||
|
||||
// Tag engines (SPHINCS+)
|
||||
TagEngine<Signature> tagEnc = TagEngineBuilder.sphincsPlusSign(spx.getPrivate()).get();
|
||||
TagEngine<Signature> tagDec = TagEngineBuilder.sphincsPlusVerify(spx.getPublic()).get();
|
||||
TagEngine<Signature> tagEnc = TagEngineBuilder.sphincsPlusSign(new zeroecho.sdk.ZeroEchoSession(), spx.getPrivate()).get();
|
||||
TagEngine<Signature> tagDec = TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), 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();
|
||||
.add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader())
|
||||
.add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build();
|
||||
|
||||
byte[] pt = readAll(dec.getStream());
|
||||
@@ -249,28 +251,28 @@ public class TagTrailerDataContentBuilderTest {
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
// AES key
|
||||
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
|
||||
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyGenerator(AesKeyGenSpec.class)
|
||||
.generateSecret(AesKeyGenSpec.aes256());
|
||||
|
||||
// RSA-2048 keys (use registry for convenience)
|
||||
KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
KeyPair rsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("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();
|
||||
TagEngine<Signature> tagEnc = TagEngineBuilder.rsaSign(new zeroecho.sdk.ZeroEchoSession(), rsa.getPrivate(), pss).get();
|
||||
TagEngine<Signature> tagDec = TagEngineBuilder.rsaVerify(new zeroecho.sdk.ZeroEchoSession(), 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();
|
||||
.add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader())
|
||||
.add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build();
|
||||
|
||||
byte[] pt = readAll(dec.getStream());
|
||||
@@ -289,14 +291,14 @@ public class TagTrailerDataContentBuilderTest {
|
||||
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);
|
||||
SymmetricKeyGenerator<AesKeyGenSpec> gen = CryptoAlgorithms.require("AES")
|
||||
.symmetricKeyGenerator(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
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192))
|
||||
.add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()) // writes IV/AAD headers
|
||||
// into stream
|
||||
.build();
|
||||
|
||||
@@ -305,9 +307,9 @@ public class TagTrailerDataContentBuilderTest {
|
||||
|
||||
// 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
|
||||
.add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()) // reads IV/AAD headers
|
||||
// back
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192)
|
||||
.throwOnMismatch())
|
||||
.build();
|
||||
|
||||
@@ -328,12 +330,12 @@ public class TagTrailerDataContentBuilderTest {
|
||||
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());
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("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()))
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192))
|
||||
.add(RsaEncDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).oaep(RsaEncSpec.Hash.SHA256).withPublicKey(kp.getPublic()))
|
||||
.build();
|
||||
|
||||
byte[] ct = readAll(enc.getStream());
|
||||
@@ -341,8 +343,8 @@ public class TagTrailerDataContentBuilderTest {
|
||||
|
||||
// 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)
|
||||
.add(RsaEncDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).oaep(RsaEncSpec.Hash.SHA256).withPrivateKey(kp.getPrivate()))
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192)
|
||||
.throwOnMismatch())
|
||||
.build();
|
||||
|
||||
@@ -377,12 +379,12 @@ public class TagTrailerDataContentBuilderTest {
|
||||
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
|
||||
AesDataContentBuilder aesEnc = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192))
|
||||
.add(KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).kem(kemId).recipientPublic(kemKeys.getPublic()).derivedKeyBytes(32) // AES-256
|
||||
// key
|
||||
// derived
|
||||
// from
|
||||
@@ -396,14 +398,14 @@ public class TagTrailerDataContentBuilderTest {
|
||||
System.out.println("...envelope=" + envelope.length + " bytes");
|
||||
|
||||
// DECRYPT: [source(envelope)] -> [KEM] -> [tag verify]
|
||||
AesDataContentBuilder aesDec = AesDataContentBuilder.builder().modeGcm(128).withHeader();
|
||||
AesDataContentBuilder aesDec = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader();
|
||||
|
||||
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(envelope))
|
||||
.add(KemDataContentBuilder.builder().kem(kemId).recipientPrivate(kemKeys.getPrivate())
|
||||
.add(KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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)
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192)
|
||||
.throwOnMismatch())
|
||||
.build();
|
||||
|
||||
@@ -428,26 +430,26 @@ public class TagTrailerDataContentBuilderTest {
|
||||
|
||||
// --- recipients ---
|
||||
// RSA
|
||||
KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
KeyPair rsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
// ML-KEM (Kyber768 as a good mid-level)
|
||||
KeyPair kem = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair kem = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("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
|
||||
AesDataContentBuilder aesEnc = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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);
|
||||
TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192);
|
||||
|
||||
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kem.getPublic());
|
||||
EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kem.getPublic());
|
||||
|
||||
// --- envelope (ENCRYPT) with 3 recipients ---
|
||||
MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesEnc)
|
||||
MultiRecipientDataSourceBuilder envEnc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesEnc)
|
||||
// .addRsaOaepRecipient(rsa.getPublic()) // RSA-OAEP with SHA-256 MGF1
|
||||
// .addKemRecipient("ML-KEM", kem.getPublic(), 32 /* kekBytes */, 16 /*
|
||||
// hkdfSaltLen */)
|
||||
@@ -465,35 +467,35 @@ public class TagTrailerDataContentBuilderTest {
|
||||
// -------------- Decrypt three ways on the same ciphertext --------------
|
||||
|
||||
// a) by RSA private key
|
||||
AesDataContentBuilder aesDecRsa = AesDataContentBuilder.builder().modeGcm(128).withHeader(); // read header to
|
||||
AesDataContentBuilder aesDecRsa = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); // read header to
|
||||
// recover
|
||||
// IV/tagBits
|
||||
MultiRecipientDataSourceBuilder envDecRsa = new MultiRecipientDataSourceBuilder().withAes(aesDecRsa)
|
||||
MultiRecipientDataSourceBuilder envDecRsa = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).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)
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), 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)
|
||||
AesDataContentBuilder aesDecKem = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader();
|
||||
MultiRecipientDataSourceBuilder envDecKem = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).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)
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), 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)
|
||||
AesDataContentBuilder aesDecPwd = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader();
|
||||
MultiRecipientDataSourceBuilder envDecPwd = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).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)
|
||||
.add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192)
|
||||
.throwOnMismatch())
|
||||
.build().getStream());
|
||||
System.out.println("...decrypted(PASSWORD)=" + ptPwd.length);
|
||||
@@ -514,23 +516,23 @@ public class TagTrailerDataContentBuilderTest {
|
||||
byte[] msg = random(SIZE);
|
||||
System.out.println("...input=" + msg.length);
|
||||
|
||||
KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
KeyPair rsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
|
||||
// AES-256/CBC with header so IV/params are serialized by the AES stage
|
||||
AesDataContentBuilder aesCbc = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
|
||||
AesDataContentBuilder aesCbc = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader();
|
||||
|
||||
TagTrailerDataContentBuilder<byte[]> tagEnc = new TagTrailerDataContentBuilder<>(
|
||||
TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192);
|
||||
TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192);
|
||||
|
||||
TagTrailerDataContentBuilder<byte[]> tagDec = new TagTrailerDataContentBuilder<>(
|
||||
TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)
|
||||
TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192)
|
||||
// explicit for clarity
|
||||
.throwOnMismatch();
|
||||
|
||||
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
|
||||
// Envelope: recipient table (RSA-OAEP) + AES payload (CBC/PKCS7 with header)
|
||||
MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesCbc)
|
||||
MultiRecipientDataSourceBuilder envEnc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc)
|
||||
// CEK length for AES-256
|
||||
.payloadKeyBytes(32)
|
||||
// .addRsaOaepRecipient(rsa.getPublic()); old API
|
||||
@@ -543,8 +545,8 @@ public class TagTrailerDataContentBuilderTest {
|
||||
byte[] encrypted = readAll(encTail.getStream());
|
||||
System.out.println("...encrypted=" + encrypted.length);
|
||||
|
||||
MultiRecipientDataSourceBuilder envDec = new MultiRecipientDataSourceBuilder()
|
||||
.withAes(AesDataContentBuilder.builder().modeCbcPkcs5()
|
||||
MultiRecipientDataSourceBuilder envDec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000)))
|
||||
.withAes(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5()
|
||||
// must match encrypt side
|
||||
.withHeader())
|
||||
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate()));
|
||||
@@ -571,27 +573,27 @@ public class TagTrailerDataContentBuilderTest {
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
// AES key
|
||||
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
|
||||
SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyGenerator(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);
|
||||
KeyPair ecdsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("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();
|
||||
TagEngine<Signature> tagEnc = TagEngineBuilder.ecdsaP256Sign(new zeroecho.sdk.ZeroEchoSession(), ecdsa.getPrivate()).get();
|
||||
TagEngine<Signature> tagDec = TagEngineBuilder.ecdsaP256Verify(new zeroecho.sdk.ZeroEchoSession(), 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();
|
||||
.add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader())
|
||||
.add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build();
|
||||
|
||||
byte[] pt = readAll(dec.getStream());
|
||||
@@ -612,14 +614,14 @@ public class TagTrailerDataContentBuilderTest {
|
||||
|
||||
private static KeyPair tryKeyPairWithDefaultSpec(CryptoAlgorithm alg) {
|
||||
try {
|
||||
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
|
||||
if (bi.defaultKeySpec == null) {
|
||||
for (KeyOperationInfo bi : alg.keyOperations()) {
|
||||
if (bi.operation() != KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE || bi.defaultSpec() == null) {
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType;
|
||||
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
|
||||
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(specType);
|
||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType();
|
||||
AlgorithmKeySpec spec = bi.defaultSpec();
|
||||
AsymmetricKeyPairGenerator<AlgorithmKeySpec> b = alg.asymmetricKeyPairGenerator(specType);
|
||||
KeyPair kp = b.generateKeyPair(spec);
|
||||
if (kp != null) {
|
||||
return kp;
|
||||
|
||||
@@ -218,7 +218,7 @@ class KemHybridRoundTripTest {
|
||||
new Random(123456789L).nextBytes(input);
|
||||
|
||||
// keypair via generic registry path
|
||||
KeyPair kp = CryptoAlgorithms.keyPair(kemId, keyGenSpec);
|
||||
KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair(kemId, keyGenSpec);
|
||||
|
||||
// encrypt
|
||||
DataContent enc = encryptStage(kemId, kp, mode);
|
||||
@@ -244,24 +244,24 @@ class KemHybridRoundTripTest {
|
||||
}
|
||||
|
||||
private static DataContent encryptStage(String kemId, KeyPair kp, String mode) {
|
||||
KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId).recipientPublic(kp.getPublic())
|
||||
KemDataContentBuilder kem = KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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);
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader().withAad(AAD);
|
||||
return kem.withAes(aes).build(true);
|
||||
}
|
||||
case "CBC": {
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader();
|
||||
return kem.withAes(aes).build(true);
|
||||
}
|
||||
case "CTR": {
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCtr().withHeader();
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCtr().withHeader();
|
||||
return kem.withAes(aes).build(true);
|
||||
}
|
||||
case "CHACHA20-POLY1305": {
|
||||
ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder().withAad(AAD) // non-empty → AEAD
|
||||
ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withAad(AAD) // non-empty → AEAD
|
||||
// variant
|
||||
.withHeader(); // carry nonce
|
||||
return kem.withChaCha(ch).build(true);
|
||||
@@ -272,24 +272,24 @@ class KemHybridRoundTripTest {
|
||||
}
|
||||
|
||||
private static DataContent decryptStage(String kemId, KeyPair kp, String mode) {
|
||||
KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId).recipientPrivate(kp.getPrivate())
|
||||
KemDataContentBuilder kem = KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).kem(kemId).recipientPrivate(kp.getPrivate())
|
||||
.derivedKeyBytes(32);
|
||||
|
||||
switch (mode) {
|
||||
case "GCM": {
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeGcm(128).withHeader().withAad(AAD);
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader().withAad(AAD);
|
||||
return kem.withAes(aes).build(false);
|
||||
}
|
||||
case "CBC": {
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader();
|
||||
return kem.withAes(aes).build(false);
|
||||
}
|
||||
case "CTR": {
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCtr().withHeader();
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCtr().withHeader();
|
||||
return kem.withAes(aes).build(false);
|
||||
}
|
||||
case "CHACHA20-POLY1305": {
|
||||
ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder().withAad(AAD).withHeader();
|
||||
ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withAad(AAD).withHeader();
|
||||
return kem.withChaCha(ch).build(false);
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.builders.alg;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.Key;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
import zeroecho.core.audit.AuditMode;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
import zeroecho.sdk.content.builtin.PlainBytes;
|
||||
|
||||
class SessionBoundBuilderTest {
|
||||
|
||||
@Test
|
||||
void generatedKeyUsesBuilderSessionAuditSink() {
|
||||
System.out.println("generatedKeyUsesBuilderSessionAuditSink");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
ZeroEchoSession session = new ZeroEchoSession().withAuditListener(listener);
|
||||
|
||||
AesDataContentBuilder.builder(session).generateKey(128).modeGcm(128).build(true);
|
||||
|
||||
assertEquals(1, listener.keyBuilt);
|
||||
System.out.println("...keyBuiltEvents=" + listener.keyBuilt);
|
||||
System.out.println("generatedKeyUsesBuilderSessionAuditSink...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void policyDenialOccursBeforeContextCreation() {
|
||||
System.out.println("policyDenialOccursBeforeContextCreation");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
ZeroEchoSession session = new ZeroEchoSession()
|
||||
.withAuditListener(listener)
|
||||
.withPolicy((id, role, key, spec) -> {
|
||||
throw new IllegalArgumentException("controlled policy denial");
|
||||
});
|
||||
DataContent encryption = AesDataContentBuilder.builder(session)
|
||||
.withKey(new SecretKeySpec(new byte[16], "AES"))
|
||||
.modeGcm(128)
|
||||
.build(true);
|
||||
encryption.setInput(new PlainBytes(new byte[] { 1 }));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, encryption::getStream);
|
||||
assertEquals(0, listener.contextCreated);
|
||||
System.out.println("...contextCreatedEvents=0");
|
||||
System.out.println("policyDenialOccursBeforeContextCreation...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrappedContextUsesBuilderSessionAuditConfiguration() throws Exception {
|
||||
System.out.println("wrappedContextUsesBuilderSessionAuditConfiguration");
|
||||
RecordingListener listener = new RecordingListener();
|
||||
ZeroEchoSession session = new ZeroEchoSession()
|
||||
.withAuditListener(listener)
|
||||
.withAuditMode(AuditMode.WRAP);
|
||||
DataContent encryption = AesDataContentBuilder.builder(session)
|
||||
.withKey(new SecretKeySpec(new byte[16], "AES"))
|
||||
.modeGcm(128)
|
||||
.build(true);
|
||||
encryption.setInput(new PlainBytes(new byte[] { 1, 2, 3 }));
|
||||
|
||||
try (InputStream input = encryption.getStream()) {
|
||||
input.readAllBytes();
|
||||
}
|
||||
|
||||
assertEquals(1, listener.contextCreated);
|
||||
assertEquals(1, listener.contextClosed);
|
||||
System.out.println("...created=1...closed=1");
|
||||
System.out.println("wrappedContextUsesBuilderSessionAuditConfiguration...ok");
|
||||
}
|
||||
|
||||
private static final class RecordingListener implements AuditListener {
|
||||
private int keyBuilt;
|
||||
private int contextCreated;
|
||||
private int contextClosed;
|
||||
|
||||
@Override
|
||||
public void onKeyBuilt(String id, String provider, AlgorithmKeySpec spec, Key key) {
|
||||
keyBuilt++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role,
|
||||
String keyFingerprint, Map<String, Object> specMeta) {
|
||||
contextCreated++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextClosed(String contextId, long bodyBytes, long trailerBytes, long durationMillis) {
|
||||
contextClosed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,22 +33,69 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.content.builtin;
|
||||
|
||||
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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SecretPasswordTest {
|
||||
@Test
|
||||
void testGetPlainText() {
|
||||
void redactsDiagnosticsAndOwnsPasswordCharacters() {
|
||||
System.out.print("SecretPassword/redaction...");
|
||||
char[] caller = "correct horse".toCharArray();
|
||||
char[] expected = caller.clone();
|
||||
SecretPassword password = new SecretPassword(caller);
|
||||
Arrays.fill(caller, 'x');
|
||||
|
||||
int len = 12;
|
||||
assertEquals("[REDACTED]", password.toText());
|
||||
assertEquals("[REDACTED]", password.toString());
|
||||
assertArrayEquals(expected, password.chars());
|
||||
char[] copy = password.chars();
|
||||
copy[0] = 'x';
|
||||
assertArrayEquals(expected, password.chars());
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
System.out.printf("generating password, %d characters...%n", len);
|
||||
@Test
|
||||
void destroysOwnedCharactersAndRejectsSecretAccess() throws Exception {
|
||||
System.out.print("SecretPassword/destroy...");
|
||||
SecretPassword password = new SecretPassword("sensitive".toCharArray());
|
||||
assertFalse(password.isDestroyed());
|
||||
|
||||
SecretPassword sp = new SecretPassword(len);
|
||||
password.destroy();
|
||||
password.destroy();
|
||||
|
||||
System.out.printf("...string %s (length %d)%n", sp.toText(), sp.toText().length());
|
||||
assertTrue(password.isDestroyed());
|
||||
assertThrows(IllegalStateException.class, password::chars);
|
||||
assertThrows(IllegalStateException.class, password::toBytes);
|
||||
assertThrows(IllegalStateException.class, password::getStream);
|
||||
Field field = SecretPassword.class.getDeclaredField("password");
|
||||
field.setAccessible(true);
|
||||
char[] owned = (char[]) field.get(password);
|
||||
assertTrue(Arrays.equals(new char[owned.length], owned));
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
assertEquals(len, sp.toText().length());
|
||||
@Test
|
||||
void streamOwnsAndWipesItsUtf8BufferOnClose() throws Exception {
|
||||
System.out.print("SecretPassword/stream...");
|
||||
SecretPassword password = new SecretPassword("päss".toCharArray());
|
||||
InputStream stream = password.getStream();
|
||||
assertArrayEquals("päss".getBytes(StandardCharsets.UTF_8), stream.readAllBytes());
|
||||
|
||||
Field field = stream.getClass().getDeclaredField("ownedBuffer");
|
||||
field.setAccessible(true);
|
||||
byte[] buffer = (byte[]) field.get(stream);
|
||||
stream.close();
|
||||
assertTrue(Arrays.equals(new byte[buffer.length], buffer));
|
||||
System.out.println("ok");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.guard;
|
||||
|
||||
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.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.io.Util;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
|
||||
class DecryptorCekCleanupTest {
|
||||
|
||||
@Test
|
||||
void rejectedOversizedCekIsCleared() throws Exception {
|
||||
System.out.println("rejectedOversizedCekIsCleared");
|
||||
byte[] rejected = new byte[17];
|
||||
RecipientOpener opener = (entryId, entryBlob, material) -> rejected;
|
||||
Decryptor decryptor = decryptor(opener);
|
||||
decryptor.setInput(() -> new ByteArrayInputStream(envelopeHeader()));
|
||||
|
||||
assertThrows(IOException.class, decryptor::getStream);
|
||||
|
||||
assertArrayEquals(new byte[rejected.length], rejected);
|
||||
System.out.println("...rejectedBytesCleared=17");
|
||||
System.out.println("rejectedOversizedCekIsCleared...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptedCekIsClearedWhenCiphertextHeaderIsTruncated() throws Exception {
|
||||
System.out.println("acceptedCekIsClearedWhenCiphertextHeaderIsTruncated");
|
||||
byte[] accepted = new byte[16];
|
||||
RecipientOpener opener = (entryId, entryBlob, material) -> accepted;
|
||||
Decryptor decryptor = decryptor(opener);
|
||||
decryptor.setInput(() -> new ByteArrayInputStream(envelopeHeader()));
|
||||
|
||||
assertThrows(IOException.class, decryptor::getStream);
|
||||
assertArrayEquals(new byte[accepted.length], accepted);
|
||||
|
||||
System.out.println("...acceptedBytesCleared=16");
|
||||
System.out.println("acceptedCekIsClearedWhenCiphertextHeaderIsTruncated...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptedCekIsClearedWhenPayloadSetupFails() throws Exception {
|
||||
System.out.println("acceptedCekIsClearedWhenPayloadSetupFails");
|
||||
byte[] accepted = new byte[16];
|
||||
RecipientOpener opener = (entryId, entryBlob, material) -> accepted;
|
||||
Decryptor decryptor = new Decryptor(List.of(opener), new UnlockMaterial.Password(new char[] { 'p' }),
|
||||
null, null, 16, 4, 128);
|
||||
decryptor.setInput(() -> new ByteArrayInputStream(envelopeHeader()));
|
||||
|
||||
assertThrows(NullPointerException.class, decryptor::getStream);
|
||||
|
||||
assertArrayEquals(new byte[accepted.length], accepted);
|
||||
System.out.println("...failurePathCleared=true");
|
||||
System.out.println("acceptedCekIsClearedWhenPayloadSetupFails...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void preReturnFailureClosesUpstreamStream() throws Exception {
|
||||
System.out.println("preReturnFailureClosesUpstreamStream");
|
||||
ByteArrayOutputStream header = new ByteArrayOutputStream();
|
||||
Util.writePack7I(header, 5);
|
||||
TrackingInputStream input = new TrackingInputStream(header.toByteArray(), false);
|
||||
Decryptor decryptor = decryptor((entryId, entryBlob, material) -> null);
|
||||
decryptor.setInput(() -> input);
|
||||
|
||||
assertThrows(IOException.class, decryptor::getStream);
|
||||
assertTrue(input.closed);
|
||||
|
||||
System.out.println("...upstreamClosed=true");
|
||||
System.out.println("preReturnFailureClosesUpstreamStream...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanupFailureIsSuppressedOnPrimaryFailure() throws Exception {
|
||||
System.out.println("cleanupFailureIsSuppressedOnPrimaryFailure");
|
||||
ByteArrayOutputStream header = new ByteArrayOutputStream();
|
||||
Util.writePack7I(header, 5);
|
||||
TrackingInputStream input = new TrackingInputStream(header.toByteArray(), true);
|
||||
Decryptor decryptor = decryptor((entryId, entryBlob, material) -> null);
|
||||
decryptor.setInput(() -> input);
|
||||
|
||||
IOException failure = assertThrows(IOException.class, decryptor::getStream);
|
||||
assertEquals(1, failure.getSuppressed().length);
|
||||
assertTrue(input.closed);
|
||||
|
||||
System.out.println("...suppressedCleanupFailures=1");
|
||||
System.out.println("cleanupFailureIsSuppressedOnPrimaryFailure...ok");
|
||||
}
|
||||
|
||||
private static Decryptor decryptor(RecipientOpener opener) {
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder(new ZeroEchoSession()).withHeader().modeGcm(128);
|
||||
return new Decryptor(List.of(opener), new UnlockMaterial.Password(new char[] { 'p' }),
|
||||
aes, null, 16, 4, 128);
|
||||
}
|
||||
|
||||
private static byte[] envelopeHeader() throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
Util.writePack7I(output, 1);
|
||||
Util.writeUTF8(output, "controlled");
|
||||
Util.write(output, new byte[] { 1 });
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private static final class TrackingInputStream extends InputStream {
|
||||
private final ByteArrayInputStream delegate;
|
||||
private final boolean failClose;
|
||||
private boolean closed;
|
||||
|
||||
private TrackingInputStream(byte[] bytes, boolean failClose) {
|
||||
this.delegate = new ByteArrayInputStream(bytes);
|
||||
this.failClose = failClose;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return delegate.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
closed = true;
|
||||
if (failClose) {
|
||||
throw new IOException("controlled close failure");
|
||||
}
|
||||
delegate.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.guard;
|
||||
|
||||
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.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.sdk.Pbkdf2Limits;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
|
||||
class EncryptorCekAllocationTest {
|
||||
private static final int KEY_BYTES = 16;
|
||||
|
||||
@Test
|
||||
void noDecoysGenerateOnlyPayloadCek() throws Exception {
|
||||
System.out.print("EncryptorCekAllocation/noDecoysGenerateOnlyPayloadCek...");
|
||||
RecordingRandomFactory randomFactory = new RecordingRandomFactory();
|
||||
CapturingRecipient first = new CapturingRecipient(false);
|
||||
CapturingRecipient second = new CapturingRecipient(false);
|
||||
|
||||
openEncryptor(List.of(first, second), randomFactory).close();
|
||||
|
||||
assertEquals(1, randomFactory.calls());
|
||||
assertArrayEquals(repeated((byte) 1), first.cek);
|
||||
assertArrayEquals(first.cek, second.cek);
|
||||
assertAllCleared(randomFactory.outputs);
|
||||
System.out.println("...factoryCalls=" + randomFactory.calls());
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneDecoyGeneratesOneLazyDecoyCek() throws Exception {
|
||||
System.out.print("EncryptorCekAllocation/oneDecoyGeneratesOneLazyDecoyCek...");
|
||||
RecordingRandomFactory randomFactory = new RecordingRandomFactory();
|
||||
CapturingRecipient decoy = new CapturingRecipient(true);
|
||||
|
||||
openEncryptor(List.of(decoy), randomFactory).close();
|
||||
|
||||
assertEquals(2, randomFactory.calls());
|
||||
assertArrayEquals(repeated((byte) 2), decoy.cek);
|
||||
assertAllCleared(randomFactory.outputs);
|
||||
System.out.println("...factoryCalls=" + randomFactory.calls());
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedDecoysGenerateOneLazyCekAndReuseItInOrder() throws Exception {
|
||||
System.out.print("EncryptorCekAllocation/mixedDecoysGenerateOneLazyCekAndReuseItInOrder...");
|
||||
RecordingRandomFactory randomFactory = new RecordingRandomFactory();
|
||||
CapturingRecipient realBefore = new CapturingRecipient(false);
|
||||
CapturingRecipient firstDecoy = new CapturingRecipient(true);
|
||||
CapturingRecipient realAfter = new CapturingRecipient(false);
|
||||
CapturingRecipient secondDecoy = new CapturingRecipient(true);
|
||||
|
||||
openEncryptor(List.of(realBefore, firstDecoy, realAfter, secondDecoy), randomFactory).close();
|
||||
|
||||
assertEquals(2, randomFactory.calls());
|
||||
assertArrayEquals(repeated((byte) 1), realBefore.cek);
|
||||
assertArrayEquals(realBefore.cek, realAfter.cek);
|
||||
assertArrayEquals(repeated((byte) 2), firstDecoy.cek);
|
||||
assertArrayEquals(firstDecoy.cek, secondDecoy.cek);
|
||||
assertAllCleared(randomFactory.outputs);
|
||||
System.out.println("...factoryCalls=" + randomFactory.calls());
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void recipientLimitIsCheckedBeforeCekGeneration() {
|
||||
System.out.print("EncryptorCekAllocation/recipientLimitIsCheckedBeforeCekGeneration...");
|
||||
RecordingRandomFactory randomFactory = new RecordingRandomFactory();
|
||||
Encryptor encryptor = newEncryptor(List.of(new CapturingRecipient(false), new CapturingRecipient(false)),
|
||||
1, randomFactory);
|
||||
encryptor.setInput(() -> new ByteArrayInputStream(new byte[0]));
|
||||
|
||||
assertThrows(IOException.class, encryptor::getStream);
|
||||
assertEquals(0, randomFactory.calls());
|
||||
System.out.println("...factoryCalls=0");
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void recipientFailureClearsGenuineAndDecoyCeks() {
|
||||
System.out.print("EncryptorCekAllocation/recipientFailureClearsGenuineAndDecoyCeks...");
|
||||
RecordingRandomFactory randomFactory = new RecordingRandomFactory();
|
||||
Encryptor encryptor = newEncryptor(List.of(new CapturingRecipient(false), new FailingRecipient(true)),
|
||||
8, randomFactory);
|
||||
encryptor.setInput(() -> new ByteArrayInputStream(new byte[0]));
|
||||
|
||||
assertThrows(IOException.class, encryptor::getStream);
|
||||
assertEquals(2, randomFactory.calls());
|
||||
assertAllCleared(randomFactory.outputs);
|
||||
System.out.println("...factoryCalls=" + randomFactory.calls());
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulRecipientProcessingDestroysOwnedPassword() throws Exception {
|
||||
System.out.print("EncryptorCekAllocation/successfulRecipientProcessingDestroysOwnedPassword...");
|
||||
PasswordRecipient passwordRecipient = passwordRecipient();
|
||||
|
||||
openEncryptor(List.of(passwordRecipient), new RecordingRandomFactory()).close();
|
||||
|
||||
assertTrue(passwordRecipient.isDestroyed());
|
||||
System.out.println("...passwordDestroyed=true");
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedRecipientProcessingDestroysOwnedPassword() {
|
||||
System.out.print("EncryptorCekAllocation/failedRecipientProcessingDestroysOwnedPassword...");
|
||||
PasswordRecipient passwordRecipient = passwordRecipient();
|
||||
Encryptor encryptor = newEncryptor(List.of(passwordRecipient, new FailingRecipient(false)),
|
||||
8, new RecordingRandomFactory());
|
||||
encryptor.setInput(() -> new ByteArrayInputStream(new byte[0]));
|
||||
|
||||
assertThrows(IOException.class, encryptor::getStream);
|
||||
assertTrue(passwordRecipient.isDestroyed());
|
||||
System.out.println("...passwordDestroyed=true");
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
private static InputStream openEncryptor(List<Recipient> recipients, IntFunction<byte[]> randomFactory)
|
||||
throws IOException {
|
||||
Encryptor encryptor = newEncryptor(recipients, 8, randomFactory);
|
||||
encryptor.setInput(() -> new ByteArrayInputStream(new byte[] { 1, 2, 3 }));
|
||||
return encryptor.getStream();
|
||||
}
|
||||
|
||||
private static Encryptor newEncryptor(List<Recipient> recipients, int maxRecipients,
|
||||
IntFunction<byte[]> randomFactory) {
|
||||
return new Encryptor(recipients, AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128), null, KEY_BYTES,
|
||||
maxRecipients, 1024, randomFactory);
|
||||
}
|
||||
|
||||
private static byte[] repeated(byte value) {
|
||||
byte[] result = new byte[KEY_BYTES];
|
||||
Arrays.fill(result, value);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static PasswordRecipient passwordRecipient() {
|
||||
return new PasswordRecipient(new char[] { 'p' }, Pbkdf2Limits.MINIMUM, 16, 32, false,
|
||||
new Pbkdf2Limits(20_000, 30_000));
|
||||
}
|
||||
|
||||
private static void assertAllCleared(List<byte[]> arrays) {
|
||||
for (byte[] array : arrays) {
|
||||
assertTrue(Arrays.equals(new byte[array.length], array));
|
||||
}
|
||||
}
|
||||
|
||||
private static class CapturingRecipient implements Recipient {
|
||||
private final boolean decoy;
|
||||
private byte[] cek;
|
||||
|
||||
private CapturingRecipient(boolean decoy) {
|
||||
this.decoy = decoy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean decoy() {
|
||||
return decoy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return decoy ? "test-decoy" : "test-real";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] buildRecipientEntry(byte[] inputCek) throws GeneralSecurityException {
|
||||
cek = inputCek.clone();
|
||||
return new byte[] { 1 };
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FailingRecipient extends CapturingRecipient {
|
||||
private FailingRecipient(boolean decoy) {
|
||||
super(decoy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] buildRecipientEntry(byte[] inputCek) throws GeneralSecurityException {
|
||||
throw new GeneralSecurityException("controlled recipient failure");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingRandomFactory implements IntFunction<byte[]> {
|
||||
private final List<byte[]> outputs = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public byte[] apply(int length) {
|
||||
byte[] result = new byte[length];
|
||||
Arrays.fill(result, (byte) (outputs.size() + 1));
|
||||
outputs.add(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private int calls() {
|
||||
return outputs.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.guard;
|
||||
|
||||
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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.io.IOException;
|
||||
import java.security.Key;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.NullKey;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.sdk.Pbkdf2Limits;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
|
||||
class KemRecipientLifecycleTest {
|
||||
private static final CryptoAlgorithm ALGORITHM = new TestAlgorithm();
|
||||
|
||||
@Test
|
||||
void supportedKekSizesClearSenderSharedSecrets() throws Exception {
|
||||
System.out.println("supportedKekSizesClearSenderSharedSecrets");
|
||||
for (int kekBytes : new int[] { 16, 32 }) {
|
||||
byte[] senderSecret = filled(32, (byte) 0x41);
|
||||
ControlledKemContext sender = new ControlledKemContext(senderSecret);
|
||||
byte[] cek = filled(32, (byte) 0x52);
|
||||
|
||||
byte[] entry = new KemCtxRecipient(sender, kekBytes, 16).buildRecipientEntry(cek);
|
||||
assertTrue(entry.length > cek.length);
|
||||
assertArrayEquals(new byte[senderSecret.length], senderSecret);
|
||||
assertTrue(sender.closed);
|
||||
System.out.println("...kekBytes=" + kekBytes);
|
||||
}
|
||||
System.out.println("...senderSecretsCleared=true");
|
||||
System.out.println("supportedKekSizesClearSenderSharedSecrets...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorsAndBuilderRejectUnsupportedKekBeforeOwnershipTransfer() throws Exception {
|
||||
System.out.println("constructorsAndBuilderRejectUnsupportedKekBeforeOwnershipTransfer");
|
||||
int[] invalidValues = { -1, 0, 1, 15, 17, 24, 31, 33, Integer.MAX_VALUE };
|
||||
ZeroEchoSession session = new ZeroEchoSession()
|
||||
.withPbkdf2Limits(new Pbkdf2Limits(20_000, 30_000));
|
||||
MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader());
|
||||
for (int kekBytes : invalidValues) {
|
||||
ControlledKemContext direct = new ControlledKemContext(filled(32, (byte) 0x11));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new KemCtxRecipient(direct, kekBytes, 16));
|
||||
assertFalse(direct.closed);
|
||||
direct.close();
|
||||
|
||||
ControlledKemContext normal = new ControlledKemContext(filled(32, (byte) 0x22));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> builder.addRecipient(normal, kekBytes, 16));
|
||||
assertEquals(0, recipients(builder).size());
|
||||
assertFalse(normal.closed);
|
||||
normal.close();
|
||||
|
||||
ControlledKemContext decoy = new ControlledKemContext(filled(32, (byte) 0x33));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> builder.addRecipientDecoy(decoy, kekBytes, 16));
|
||||
assertEquals(0, recipients(builder).size());
|
||||
assertFalse(decoy.closed);
|
||||
decoy.close();
|
||||
}
|
||||
builder.close();
|
||||
System.out.println("...recipientCount=0");
|
||||
System.out.println("constructorsAndBuilderRejectUnsupportedKekBeforeOwnershipTransfer...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapFailureClearsSharedSecretAndClosesContext() {
|
||||
System.out.println("wrapFailureClearsSharedSecretAndClosesContext");
|
||||
byte[] sharedSecret = filled(32, (byte) 0x33);
|
||||
ControlledKemContext context = new ControlledKemContext(sharedSecret, ALGORITHM, null);
|
||||
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new KemCtxRecipient(context, 16, 16).buildRecipientEntry(new byte[32]));
|
||||
assertArrayEquals(new byte[sharedSecret.length], sharedSecret);
|
||||
assertTrue(context.closed);
|
||||
System.out.println("...failureCleanup=true");
|
||||
System.out.println("wrapFailureClearsSharedSecretAndClosesContext...ok");
|
||||
}
|
||||
|
||||
private static byte[] filled(int length, byte value) {
|
||||
byte[] result = new byte[length];
|
||||
java.util.Arrays.fill(result, value);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final class TestAlgorithm extends CryptoAlgorithm {
|
||||
private TestAlgorithm() {
|
||||
super("TEST-KEM", "Test KEM");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ControlledKemContext implements KemContext {
|
||||
private final byte[] sharedSecret;
|
||||
private final CryptoAlgorithm algorithm;
|
||||
private final byte[] ciphertext;
|
||||
private boolean closed;
|
||||
|
||||
private ControlledKemContext(byte[] sharedSecret) {
|
||||
this(sharedSecret, ALGORITHM, new byte[] { 1, 2, 3 });
|
||||
}
|
||||
|
||||
private ControlledKemContext(byte[] sharedSecret, CryptoAlgorithm algorithm, byte[] ciphertext) {
|
||||
this.sharedSecret = sharedSecret;
|
||||
this.algorithm = algorithm;
|
||||
this.ciphertext = ciphertext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KemResult encapsulate() {
|
||||
return new KemResult(ciphertext, sharedSecret);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decapsulate(byte[] ciphertext) {
|
||||
return sharedSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoAlgorithm algorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key key() {
|
||||
return NullKey.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Recipient> recipients(MultiRecipientDataSourceBuilder builder) throws Exception {
|
||||
Field field = MultiRecipientDataSourceBuilder.class.getDeclaredField("recipients");
|
||||
field.setAccessible(true);
|
||||
return (List<Recipient>) field.get(builder);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@
|
||||
package zeroecho.sdk.guard;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -43,6 +45,7 @@ import java.security.KeyPairGenerator;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
@@ -62,6 +65,8 @@ import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.tag.TagEngineBuilder;
|
||||
import zeroecho.sdk.Pbkdf2Limits;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentBuilder;
|
||||
@@ -99,10 +104,10 @@ public class MultiRecipientEnvelopeTest {
|
||||
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();
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader();
|
||||
|
||||
// Encrypt
|
||||
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
|
||||
MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get())
|
||||
.payloadKeyBytes(32)
|
||||
.addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32);
|
||||
|
||||
@@ -115,8 +120,9 @@ public class MultiRecipientEnvelopeTest {
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
|
||||
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password));
|
||||
UnlockMaterial.Password unlockMaterial = new UnlockMaterial.Password(password);
|
||||
MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get())
|
||||
.payloadKeyBytes(32).unlockWith(unlockMaterial);
|
||||
|
||||
DataContent decryptor = dec.build(false);
|
||||
decryptor.setInput(new BytesContent(encrypted));
|
||||
@@ -127,9 +133,54 @@ public class MultiRecipientEnvelopeTest {
|
||||
}
|
||||
|
||||
assertArrayEquals(input, decrypted);
|
||||
assertFalse(unlockMaterial.isDestroyed());
|
||||
unlockMaterial.destroy();
|
||||
assertTrue(unlockMaterial.isDestroyed());
|
||||
System.out.println("... borrowed unlock material retained by caller");
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void passwordRecipientWithAes128KekRoundTrips() throws Exception {
|
||||
System.out.println("passwordRecipientWithAes128KekRoundTrips");
|
||||
byte[] input = randomInput(257);
|
||||
char[] password = "controlled-password".toCharArray();
|
||||
ZeroEchoSession session = new ZeroEchoSession()
|
||||
.withPbkdf2Limits(new Pbkdf2Limits(1_000_000, 1_000_000));
|
||||
byte[] encrypted;
|
||||
|
||||
try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
|
||||
.payloadKeyBytes(32)
|
||||
.addPasswordRecipient(password, 10_000, 16, 16);
|
||||
MultiRecipientContent content = builder.build(true)) {
|
||||
content.setInput(new BytesContent(input));
|
||||
try (InputStream stream = content.getStream()) {
|
||||
encrypted = stream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
UnlockMaterial.Password unlock = new UnlockMaterial.Password(password);
|
||||
byte[] decrypted;
|
||||
try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
|
||||
.payloadKeyBytes(32)
|
||||
.unlockWith(unlock);
|
||||
MultiRecipientContent content = builder.build(false)) {
|
||||
content.setInput(new BytesContent(encrypted));
|
||||
try (InputStream stream = content.getStream()) {
|
||||
decrypted = stream.readAllBytes();
|
||||
}
|
||||
} finally {
|
||||
unlock.destroy();
|
||||
Arrays.fill(password, '\0');
|
||||
}
|
||||
|
||||
assertArrayEquals(input, decrypted);
|
||||
System.out.println("...kekBytes=16");
|
||||
System.out.println("passwordRecipientWithAes128KekRoundTrips...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Password guardian + AES-256-CBC/PKCS7")
|
||||
void testPasswordGuardian_Aes256Cbc() throws Exception {
|
||||
@@ -142,9 +193,9 @@ public class MultiRecipientEnvelopeTest {
|
||||
final char[] password = "Tr0ub4dor&3".toCharArray();
|
||||
|
||||
// AES-256-CBC with PKCS7 padding, header persists IV
|
||||
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
|
||||
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader();
|
||||
|
||||
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
|
||||
MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get())
|
||||
.payloadKeyBytes(32)
|
||||
.addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32);
|
||||
|
||||
@@ -156,7 +207,7 @@ public class MultiRecipientEnvelopeTest {
|
||||
encrypted = readAllBytesAndPrint(es, "... encrypted size");
|
||||
}
|
||||
|
||||
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
|
||||
MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get())
|
||||
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password));
|
||||
|
||||
DataContent decryptor = dec.build(false);
|
||||
@@ -184,12 +235,12 @@ public class MultiRecipientEnvelopeTest {
|
||||
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());
|
||||
KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
|
||||
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader();
|
||||
|
||||
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
|
||||
MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get())
|
||||
.payloadKeyBytes(32).addRecipient(elgEnc);
|
||||
|
||||
DataContent encryptor = enc.build(true);
|
||||
@@ -200,7 +251,7 @@ public class MultiRecipientEnvelopeTest {
|
||||
encrypted = readAllBytesAndPrint(es, "... encrypted size");
|
||||
}
|
||||
|
||||
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
|
||||
MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get())
|
||||
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(elg.getPrivate()));
|
||||
|
||||
DataContent decryptor = dec.build(false);
|
||||
@@ -224,12 +275,12 @@ public class MultiRecipientEnvelopeTest {
|
||||
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());
|
||||
KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
|
||||
|
||||
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
|
||||
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader();
|
||||
|
||||
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
|
||||
MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get())
|
||||
.payloadKeyBytes(32).addRecipient(elgEnc);
|
||||
|
||||
DataContent encryptor = enc.build(true);
|
||||
@@ -240,7 +291,7 @@ public class MultiRecipientEnvelopeTest {
|
||||
encrypted = readAllBytesAndPrint(es, "... encrypted size");
|
||||
}
|
||||
|
||||
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
|
||||
MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get())
|
||||
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(elg.getPrivate()));
|
||||
|
||||
DataContent decryptor = dec.build(false);
|
||||
@@ -272,11 +323,11 @@ public class MultiRecipientEnvelopeTest {
|
||||
kpg.initialize(3072, new SecureRandom());
|
||||
KeyPair rsa = kpg.generateKeyPair();
|
||||
|
||||
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader();
|
||||
|
||||
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
|
||||
MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get())
|
||||
.payloadKeyBytes(32).addRecipient(rsaEnc);
|
||||
|
||||
DataContent encryptor = enc.build(true);
|
||||
@@ -287,7 +338,7 @@ public class MultiRecipientEnvelopeTest {
|
||||
encrypted = readAllBytesAndPrint(es, "... encrypted size");
|
||||
}
|
||||
|
||||
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
|
||||
MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get())
|
||||
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate()));
|
||||
|
||||
DataContent decryptor = dec.build(false);
|
||||
@@ -315,11 +366,11 @@ public class MultiRecipientEnvelopeTest {
|
||||
kpg.initialize(3072, new SecureRandom());
|
||||
KeyPair rsa = kpg.generateKeyPair();
|
||||
|
||||
EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
|
||||
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
|
||||
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader();
|
||||
|
||||
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
|
||||
MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get())
|
||||
.payloadKeyBytes(32).addRecipient(rsaEnc);
|
||||
|
||||
DataContent encryptor = enc.build(true);
|
||||
@@ -330,7 +381,7 @@ public class MultiRecipientEnvelopeTest {
|
||||
encrypted = readAllBytesAndPrint(es, "... encrypted size");
|
||||
}
|
||||
|
||||
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
|
||||
MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get())
|
||||
.payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate()));
|
||||
|
||||
DataContent decryptor = dec.build(false);
|
||||
@@ -349,6 +400,129 @@ public class MultiRecipientEnvelopeTest {
|
||||
// Multi-recipient (Password + RSA + KEM/ML-KEM + ElGamal) via contexts
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testKemRecipientWith128BitKekRoundTrip() throws Exception {
|
||||
System.out.println("testKemRecipientWith128BitKekRoundTrip");
|
||||
byte[] input = randomInput(257);
|
||||
KeyPair keyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric()
|
||||
.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512());
|
||||
|
||||
byte[] encrypted = encryptKemRecipients(input, new KeyPair[] { keyPair }, new int[] { 16 });
|
||||
decryptAndAssert("...128-bit KEK", aesGcmSupplier(), new UnlockMaterial.Private(keyPair.getPrivate()),
|
||||
input, encrypted);
|
||||
|
||||
System.out.println("...encryptedLength=" + encrypted.length);
|
||||
System.out.println("testKemRecipientWith128BitKekRoundTrip...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedKemRecipientKekSizesRoundTrip() throws Exception {
|
||||
System.out.println("testMixedKemRecipientKekSizesRoundTrip");
|
||||
zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession();
|
||||
KeyPair first = session.keyBuilders().asymmetric()
|
||||
.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512());
|
||||
KeyPair second = session.keyBuilders().asymmetric()
|
||||
.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512());
|
||||
byte[] input = randomInput(513);
|
||||
|
||||
byte[] encrypted = encryptKemRecipients(input, new KeyPair[] { first, second }, new int[] { 16, 32 });
|
||||
decryptAndAssert("...mixed 16-byte KEK", aesGcmSupplier(),
|
||||
new UnlockMaterial.Private(first.getPrivate()), input, encrypted);
|
||||
decryptAndAssert("...mixed 32-byte KEK", aesGcmSupplier(),
|
||||
new UnlockMaterial.Private(second.getPrivate()), input, encrypted);
|
||||
|
||||
System.out.println("...recipientCount=2");
|
||||
System.out.println("testMixedKemRecipientKekSizesRoundTrip...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultKemOpenerContinuesAfterSameAlgorithmDecoy() throws Exception {
|
||||
System.out.println("defaultKemOpenerContinuesAfterSameAlgorithmDecoy");
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
KeyPair decoy = session.keyBuilders().asymmetric()
|
||||
.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512());
|
||||
KeyPair legitimate = session.keyBuilders().asymmetric()
|
||||
.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512());
|
||||
byte[] input = randomInput(385);
|
||||
byte[] encrypted;
|
||||
|
||||
KemContext decoyContext =
|
||||
session.createContext("ML-KEM", KeyUsage.ENCAPSULATE, decoy.getPublic());
|
||||
KemContext legitimateContext =
|
||||
session.createContext("ML-KEM", KeyUsage.ENCAPSULATE, legitimate.getPublic());
|
||||
try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
|
||||
.payloadKeyBytes(32)
|
||||
.addRecipient(decoyContext, 32, 16)
|
||||
.addRecipient(legitimateContext, 32, 16);
|
||||
MultiRecipientContent content = builder.build(true)) {
|
||||
content.setInput(new BytesContent(input));
|
||||
try (InputStream stream = content.getStream()) {
|
||||
encrypted = stream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
byte[] decrypted;
|
||||
try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
|
||||
.payloadKeyBytes(32)
|
||||
.unlockWith(new UnlockMaterial.Private(legitimate.getPrivate()));
|
||||
MultiRecipientContent content = builder.build(false)) {
|
||||
content.setInput(new BytesContent(encrypted));
|
||||
try (InputStream stream = content.getStream()) {
|
||||
decrypted = stream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
assertArrayEquals(input, decrypted);
|
||||
System.out.println("...sameAlgorithmEntries=2");
|
||||
System.out.println("defaultKemOpenerContinuesAfterSameAlgorithmDecoy...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultEncryptionOpenerContinuesAfterSameAlgorithmDecoy() throws Exception {
|
||||
System.out.println("defaultEncryptionOpenerContinuesAfterSameAlgorithmDecoy");
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
KeyPair decoy = session.keyBuilders().asymmetric()
|
||||
.generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
KeyPair legitimate = session.keyBuilders().asymmetric()
|
||||
.generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
byte[] input = randomInput(385);
|
||||
byte[] encrypted;
|
||||
|
||||
EncryptionContext decoyContext =
|
||||
session.createContext("RSA", KeyUsage.ENCRYPT, decoy.getPublic());
|
||||
EncryptionContext legitimateContext =
|
||||
session.createContext("RSA", KeyUsage.ENCRYPT, legitimate.getPublic());
|
||||
try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
|
||||
.payloadKeyBytes(32)
|
||||
.addRecipient(decoyContext)
|
||||
.addRecipient(legitimateContext);
|
||||
MultiRecipientContent content = builder.build(true)) {
|
||||
content.setInput(new BytesContent(input));
|
||||
try (InputStream stream = content.getStream()) {
|
||||
encrypted = stream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
byte[] decrypted;
|
||||
try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
|
||||
.payloadKeyBytes(32)
|
||||
.unlockWith(new UnlockMaterial.Private(legitimate.getPrivate()));
|
||||
MultiRecipientContent content = builder.build(false)) {
|
||||
content.setInput(new BytesContent(encrypted));
|
||||
try (InputStream stream = content.getStream()) {
|
||||
decrypted = stream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
assertArrayEquals(input, decrypted);
|
||||
System.out.println("...sameAlgorithmEntries=2");
|
||||
System.out.println("defaultEncryptionOpenerContinuesAfterSameAlgorithmDecoy...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Multi recipients (PWD+RSA+ML-KEM kyber512+ElGamal) + AES-256-GCM")
|
||||
void testMultiRecipients_Kyber512_Aes256Gcm_AllUnlocks() throws Exception {
|
||||
@@ -364,16 +538,16 @@ public class MultiRecipientEnvelopeTest {
|
||||
kpg.initialize(3072, new SecureRandom());
|
||||
KeyPair rsa = kpg.generateKeyPair();
|
||||
|
||||
KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber512());
|
||||
KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
KeyPair kyber = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512());
|
||||
KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("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());
|
||||
EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
|
||||
KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic());
|
||||
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader();
|
||||
|
||||
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
|
||||
MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get())
|
||||
.payloadKeyBytes(32)
|
||||
.addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32)
|
||||
.addRecipient(rsaEnc).addRecipient(elgEnc).addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32);
|
||||
@@ -412,17 +586,17 @@ public class MultiRecipientEnvelopeTest {
|
||||
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());
|
||||
rsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
KeyPair kyber = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("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());
|
||||
EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
|
||||
KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic());
|
||||
|
||||
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader();
|
||||
Supplier<AesDataContentBuilder> aesCbc = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader();
|
||||
|
||||
MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get())
|
||||
MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get())
|
||||
.payloadKeyBytes(32)
|
||||
.addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32)
|
||||
.addRecipient(rsaEnc).addRecipient(elgEnc).addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32);
|
||||
@@ -463,29 +637,29 @@ public class MultiRecipientEnvelopeTest {
|
||||
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());
|
||||
KeyPair kyber = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
|
||||
// Sender signature keys (Ed25519)
|
||||
KeyPair ed = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
|
||||
KeyPair ed = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
|
||||
|
||||
// AES-256-GCM payload builder
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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());
|
||||
EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
|
||||
KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic());
|
||||
|
||||
// Envelope (encrypt)
|
||||
MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
|
||||
MultiRecipientDataSourceBuilder envEnc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).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);
|
||||
TagEngineBuilder.ed25519Sign(new zeroecho.sdk.ZeroEchoSession(), ed.getPrivate())).bufferSize(8192);
|
||||
|
||||
// Encrypt chain
|
||||
DataContent encryptChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(signTrailer)
|
||||
@@ -500,10 +674,10 @@ public class MultiRecipientEnvelopeTest {
|
||||
TagTrailerDataContentBuilder<Signature> verifyTrailer;
|
||||
|
||||
// via Password
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic()))
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), ed.getPublic()))
|
||||
.bufferSize(8192).throwOnMismatch();
|
||||
DataContent decPwd = DataContentChainBuilder
|
||||
.decrypt().add(BytesSourceBuilder.of(ciphertext)).add(new MultiRecipientDataSourceBuilder()
|
||||
.decrypt().add(BytesSourceBuilder.of(ciphertext)).add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000)))
|
||||
.withAes(aesGcm.get()).payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password)))
|
||||
.add(verifyTrailer).build();
|
||||
byte[] ptPwd;
|
||||
@@ -513,10 +687,10 @@ public class MultiRecipientEnvelopeTest {
|
||||
assertArrayEquals(msg, ptPwd);
|
||||
|
||||
// via RSA
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic()))
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), ed.getPublic()))
|
||||
.bufferSize(8192).throwOnMismatch();
|
||||
DataContent decRsa = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
|
||||
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.unlockWith(new UnlockMaterial.Private(rsa.getPrivate())))
|
||||
.add(verifyTrailer).build();
|
||||
byte[] ptRsa;
|
||||
@@ -526,10 +700,10 @@ public class MultiRecipientEnvelopeTest {
|
||||
assertArrayEquals(msg, ptRsa);
|
||||
|
||||
// via ElGamal
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic()))
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), ed.getPublic()))
|
||||
.bufferSize(8192).throwOnMismatch();
|
||||
DataContent decElgamal = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
|
||||
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.unlockWith(new UnlockMaterial.Private(elg.getPrivate())))
|
||||
.add(verifyTrailer).build();
|
||||
byte[] ptElgamal;
|
||||
@@ -539,10 +713,10 @@ public class MultiRecipientEnvelopeTest {
|
||||
assertArrayEquals(msg, ptElgamal);
|
||||
|
||||
// via ML-KEM
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic()))
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), ed.getPublic()))
|
||||
.bufferSize(8192).throwOnMismatch();
|
||||
DataContent decKem = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
|
||||
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.unlockWith(new UnlockMaterial.Private(kyber.getPrivate())))
|
||||
.add(verifyTrailer).build();
|
||||
byte[] ptKem;
|
||||
@@ -568,28 +742,28 @@ public class MultiRecipientEnvelopeTest {
|
||||
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());
|
||||
KeyPair kyber = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512());
|
||||
KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048());
|
||||
|
||||
// Sender signature keys (SPHINCS+, default/best)
|
||||
KeyPair spx = CryptoAlgorithms.keyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec());
|
||||
KeyPair spx = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec());
|
||||
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader();
|
||||
Supplier<AesDataContentBuilder> aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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());
|
||||
EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic());
|
||||
EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic());
|
||||
KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic());
|
||||
|
||||
// Envelope recipients
|
||||
MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get())
|
||||
MultiRecipientDataSourceBuilder envEnc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).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);
|
||||
TagEngineBuilder.sphincsPlusSign(new zeroecho.sdk.ZeroEchoSession(), spx.getPrivate())).bufferSize(8192);
|
||||
|
||||
// Encrypt chain
|
||||
DataContent encryptChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(signTrailer)
|
||||
@@ -603,10 +777,10 @@ public class MultiRecipientEnvelopeTest {
|
||||
TagTrailerDataContentBuilder<Signature> verifyTrailer;
|
||||
|
||||
// via Password
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic()))
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), spx.getPublic()))
|
||||
.bufferSize(8192).throwOnMismatch();
|
||||
DataContent decPwd = DataContentChainBuilder
|
||||
.decrypt().add(BytesSourceBuilder.of(ciphertext)).add(new MultiRecipientDataSourceBuilder()
|
||||
.decrypt().add(BytesSourceBuilder.of(ciphertext)).add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000)))
|
||||
.withAes(aesGcm.get()).payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password)))
|
||||
.add(verifyTrailer).build();
|
||||
byte[] ptPwd;
|
||||
@@ -616,10 +790,10 @@ public class MultiRecipientEnvelopeTest {
|
||||
assertArrayEquals(msg, ptPwd);
|
||||
|
||||
// via RSA
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic()))
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), spx.getPublic()))
|
||||
.bufferSize(8192).throwOnMismatch();
|
||||
DataContent decRsa = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
|
||||
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.unlockWith(new UnlockMaterial.Private(rsa.getPrivate())))
|
||||
.add(verifyTrailer).build();
|
||||
byte[] ptRsa;
|
||||
@@ -629,10 +803,10 @@ public class MultiRecipientEnvelopeTest {
|
||||
assertArrayEquals(msg, ptRsa);
|
||||
|
||||
// via ElGamal
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic()))
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), spx.getPublic()))
|
||||
.bufferSize(8192).throwOnMismatch();
|
||||
DataContent decElgamal = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
|
||||
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.unlockWith(new UnlockMaterial.Private(elg.getPrivate())))
|
||||
.add(verifyTrailer).build();
|
||||
byte[] ptElgamal;
|
||||
@@ -642,10 +816,10 @@ public class MultiRecipientEnvelopeTest {
|
||||
assertArrayEquals(msg, ptElgamal);
|
||||
|
||||
// via ML-KEM
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic()))
|
||||
verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), spx.getPublic()))
|
||||
.bufferSize(8192).throwOnMismatch();
|
||||
DataContent decKem = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext))
|
||||
.add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32)
|
||||
.unlockWith(new UnlockMaterial.Private(kyber.getPrivate())))
|
||||
.add(verifyTrailer).build();
|
||||
byte[] ptKem;
|
||||
@@ -661,6 +835,28 @@ public class MultiRecipientEnvelopeTest {
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
private static byte[] encryptKemRecipients(byte[] input, KeyPair[] keyPairs, int[] kekSizes)
|
||||
throws Exception {
|
||||
zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession();
|
||||
MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(aesGcmSupplier().get()).payloadKeyBytes(32);
|
||||
for (int index = 0; index < keyPairs.length; index++) {
|
||||
KemContext context = session.createContext("ML-KEM", KeyUsage.ENCAPSULATE,
|
||||
keyPairs[index].getPublic());
|
||||
builder.addRecipient(context, kekSizes[index], 16);
|
||||
}
|
||||
DataContent encryptor = builder.build(true);
|
||||
encryptor.setInput(new BytesContent(input));
|
||||
try (InputStream stream = encryptor.getStream()) {
|
||||
return stream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private static Supplier<AesDataContentBuilder> aesGcmSupplier() {
|
||||
return () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession())
|
||||
.modeGcm(128).withHeader();
|
||||
}
|
||||
|
||||
/** Minimal source builder so we can compose pull-style chains. */
|
||||
private static final class BytesSourceBuilder implements DataContentBuilder<PlainContent> {
|
||||
private final byte[] data;
|
||||
@@ -690,7 +886,7 @@ public class MultiRecipientEnvelopeTest {
|
||||
|
||||
private static void decryptAndAssert(String banner, Supplier<AesDataContentBuilder> aesFactory,
|
||||
UnlockMaterial material, byte[] original, byte[] encrypted) throws IOException {
|
||||
MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesFactory.get())
|
||||
MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesFactory.get())
|
||||
.payloadKeyBytes(32).unlockWith(material);
|
||||
|
||||
DataContent decryptor = dec.build(false);
|
||||
|
||||
251
lib/src/test/java/zeroecho/sdk/guard/PasswordRecipientTest.java
Normal file
251
lib/src/test/java/zeroecho/sdk/guard/PasswordRecipientTest.java
Normal file
@@ -0,0 +1,251 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.guard;
|
||||
|
||||
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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.io.Util;
|
||||
import zeroecho.sdk.Pbkdf2Limits;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
|
||||
class PasswordRecipientTest {
|
||||
private static final Pbkdf2Limits LIMITS = new Pbkdf2Limits(20_000, 30_000);
|
||||
|
||||
@Test
|
||||
void ownsAndDestroysRetainedPassword() throws Exception {
|
||||
System.out.print("PasswordRecipient/lifecycle...");
|
||||
char[] caller = "envelope-password".toCharArray();
|
||||
char[] expected = caller.clone();
|
||||
PasswordRecipient recipient = new PasswordRecipient(caller, 10_000, 16, 32, false, LIMITS);
|
||||
Arrays.fill(caller, 'x');
|
||||
|
||||
Field field = PasswordRecipient.class.getDeclaredField("password");
|
||||
field.setAccessible(true);
|
||||
char[] owned = (char[]) field.get(recipient);
|
||||
assertArrayEquals(expected, owned);
|
||||
|
||||
recipient.destroy();
|
||||
recipient.destroy();
|
||||
assertTrue(recipient.isDestroyed());
|
||||
assertTrue(Arrays.equals(new char[owned.length], owned));
|
||||
assertThrows(IllegalStateException.class, () -> recipient.buildRecipientEntry(new byte[32]));
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorsRejectIterationsBelowMinimumAndAcceptBoundary() {
|
||||
System.out.print("PasswordRecipient/constructorsRejectIterationsBelowMinimumAndAcceptBoundary...");
|
||||
int[] invalidValues = { -1, 0, 1, 9_999 };
|
||||
for (int iterations : invalidValues) {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new PasswordRecipient(new char[] { 'p' }, iterations, 16, 32, false, LIMITS));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new PasswordRecipient(new char[] { 'p' }, iterations, 16, 32, true, LIMITS));
|
||||
}
|
||||
|
||||
new PasswordRecipient(new char[] { 'p' }, 10_000, 16, 32, false, LIMITS);
|
||||
new PasswordRecipient(new char[] { 'p' }, 10_001, 16, 32, true, LIMITS);
|
||||
System.out.println("...minimum=" + Pbkdf2Limits.MINIMUM);
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorsAcceptOnlyOpenableKekSizes() throws Exception {
|
||||
System.out.println("constructorsAcceptOnlyOpenableKekSizes");
|
||||
int[] invalidValues = { -1, 0, 1, 15, 17, 24, 31, 33, Integer.MAX_VALUE };
|
||||
for (int kekBytes : invalidValues) {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new PasswordRecipient(new char[] { 'p' }, 10_000, 16, kekBytes, false, LIMITS));
|
||||
}
|
||||
|
||||
PasswordRecipient aes128 =
|
||||
new PasswordRecipient(new char[] { 'p' }, 10_000, 16, 16, false, LIMITS);
|
||||
PasswordRecipient aes256 =
|
||||
new PasswordRecipient(new char[] { 'p' }, 10_000, 16, 32, false, LIMITS);
|
||||
aes128.close();
|
||||
aes256.close();
|
||||
System.out.println("...acceptedKekBytes=16,32");
|
||||
System.out.println("constructorsAcceptOnlyOpenableKekSizes...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void builderMethodsRejectBeforeRecipientListMutation() throws Exception {
|
||||
System.out.print("PasswordRecipient/builderMethodsRejectBeforeRecipientListMutation...");
|
||||
MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder
|
||||
.builder(new ZeroEchoSession().withPbkdf2Limits(LIMITS));
|
||||
int[] invalidValues = { -1, 0, 1, 9_999 };
|
||||
|
||||
for (int iterations : invalidValues) {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> builder.addPasswordRecipient(new char[] { 'p' }, iterations, 16, 32));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> builder.addPasswordRecipientDecoy(new char[] { 'p' }, iterations, 16, 32));
|
||||
assertEquals(0, recipients(builder).size());
|
||||
}
|
||||
|
||||
builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, 32);
|
||||
builder.addPasswordRecipientDecoy(new char[] { 'p' }, 10_001, 16, 32);
|
||||
assertEquals(2, recipients(builder).size());
|
||||
System.out.println("...recipientCount=" + recipients(builder).size());
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void builderRejectsUnsupportedKekBeforeStateMutation() throws Exception {
|
||||
System.out.println("builderRejectsUnsupportedKekBeforeStateMutation");
|
||||
MultiRecipientDataSourceBuilder builder = builderWithAes();
|
||||
int[] invalidValues = { -1, 0, 1, 15, 17, 24, 31, 33, Integer.MAX_VALUE };
|
||||
|
||||
for (int kekBytes : invalidValues) {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, kekBytes));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> builder.addPasswordRecipientDecoy(new char[] { 'p' }, 10_000, 16, kekBytes));
|
||||
assertEquals(0, recipients(builder).size());
|
||||
}
|
||||
builder.close();
|
||||
System.out.println("...recipientCount=0");
|
||||
System.out.println("builderRejectsUnsupportedKekBeforeStateMutation...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void passwordOpenerRejectsSubminimumDecodedIterations() throws Exception {
|
||||
System.out.print("PasswordRecipient/passwordOpenerRejectsSubminimumDecodedIterations...");
|
||||
ByteArrayOutputStream blob = new ByteArrayOutputStream();
|
||||
Util.writePack7I(blob, 9_999);
|
||||
|
||||
assertThrows(IOException.class, () -> new PasswordOpener(LIMITS).tryOpen(
|
||||
"PWD:PBKDF2-SHA256:GCM-WRAP", blob.toByteArray(),
|
||||
new UnlockMaterial.Password(new char[] { 'p' })));
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void passwordOpenerRejectsUnterminatedIterationEncoding() {
|
||||
System.out.print("PasswordRecipient/passwordOpenerRejectsUnterminatedIterationEncoding...");
|
||||
PasswordOpener opener = new PasswordOpener(LIMITS);
|
||||
UnlockMaterial.Password material = new UnlockMaterial.Password(new char[] { 'p' });
|
||||
|
||||
assertThrows(IOException.class,
|
||||
() -> opener.tryOpen("PWD:PBKDF2-SHA256:GCM-WRAP", new byte[] { 1 }, material));
|
||||
assertThrows(IOException.class,
|
||||
() -> opener.tryOpen("PWD:PBKDF2-SHA256:GCM-WRAP",
|
||||
new byte[] { 1, 1, 1, 1, 1, (byte) 0x80 }, material));
|
||||
System.out.println("...malformedCases=2");
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void builderCloseDestroysAbandonedPasswordRecipients() throws Exception {
|
||||
System.out.println("builderCloseDestroysAbandonedPasswordRecipients");
|
||||
MultiRecipientDataSourceBuilder builder = builderWithAes();
|
||||
builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, 32);
|
||||
PasswordRecipient recipient = (PasswordRecipient) recipients(builder).get(0);
|
||||
|
||||
builder.close();
|
||||
builder.close();
|
||||
|
||||
assertTrue(builder.isDestroyed());
|
||||
assertTrue(recipient.isDestroyed());
|
||||
assertThrows(IllegalStateException.class, () -> builder.payloadKeyBytes(16));
|
||||
System.out.println("...destroyed=true");
|
||||
System.out.println("builderCloseDestroysAbandonedPasswordRecipients...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferredRecipientSurvivesBuilderCloseUntilTerminalFailure() throws Exception {
|
||||
System.out.println("transferredRecipientSurvivesBuilderCloseUntilTerminalFailure");
|
||||
MultiRecipientDataSourceBuilder builder = builderWithAes();
|
||||
builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, 32);
|
||||
PasswordRecipient recipient = (PasswordRecipient) recipients(builder).get(0);
|
||||
DataContent encryptor = builder.build(true);
|
||||
|
||||
builder.close();
|
||||
assertFalse(recipient.isDestroyed());
|
||||
assertThrows(NullPointerException.class, encryptor::getStream);
|
||||
assertTrue(recipient.isDestroyed());
|
||||
|
||||
System.out.println("...ownershipTransferred=true");
|
||||
System.out.println("transferredRecipientSurvivesBuilderCloseUntilTerminalFailure...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void abandonedBuiltContentOwnsAndDestroysTransferredRecipient() throws Exception {
|
||||
System.out.println("abandonedBuiltContentOwnsAndDestroysTransferredRecipient");
|
||||
MultiRecipientDataSourceBuilder builder = builderWithAes();
|
||||
builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, 32);
|
||||
PasswordRecipient recipient = (PasswordRecipient) recipients(builder).get(0);
|
||||
MultiRecipientContent content = builder.build(true);
|
||||
|
||||
builder.close();
|
||||
assertFalse(recipient.isDestroyed());
|
||||
content.close();
|
||||
content.close();
|
||||
|
||||
assertTrue(content.isDestroyed());
|
||||
assertTrue(recipient.isDestroyed());
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> content.setInput(() -> new ByteArrayInputStream(new byte[0])));
|
||||
assertThrows(IllegalStateException.class, content::getStream);
|
||||
System.out.println("...contentDestroyed=true");
|
||||
System.out.println("abandonedBuiltContentOwnsAndDestroysTransferredRecipient...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalLimitAndRandomFailuresDestroyPasswordRecipients() throws Exception {
|
||||
System.out.println("terminalLimitAndRandomFailuresDestroyPasswordRecipients");
|
||||
MultiRecipientDataSourceBuilder limitedBuilder = builderWithAes().headerLimits(1, 1024);
|
||||
limitedBuilder.addPasswordRecipient(new char[] { 'a' }, 10_000, 16, 32);
|
||||
limitedBuilder.addPasswordRecipient(new char[] { 'b' }, 10_000, 16, 32);
|
||||
List<Recipient> limitedRecipients = List.copyOf(recipients(limitedBuilder));
|
||||
DataContent limited = limitedBuilder.build(true);
|
||||
limited.setInput(() -> new ByteArrayInputStream(new byte[0]));
|
||||
assertThrows(IOException.class, limited::getStream);
|
||||
assertTrue(limitedRecipients.stream()
|
||||
.map(PasswordRecipient.class::cast).allMatch(PasswordRecipient::isDestroyed));
|
||||
|
||||
PasswordRecipient randomRecipient =
|
||||
new PasswordRecipient(new char[] { 'c' }, 10_000, 16, 32, false, LIMITS);
|
||||
Encryptor randomFailure = new Encryptor(List.of(randomRecipient),
|
||||
AesDataContentBuilder.builder(new ZeroEchoSession()).modeGcm(128).withHeader(),
|
||||
null, 32, 4, 1024, ignored -> {
|
||||
throw new IllegalStateException("controlled random failure");
|
||||
});
|
||||
randomFailure.setInput(() -> new ByteArrayInputStream(new byte[0]));
|
||||
assertThrows(IllegalStateException.class, randomFailure::getStream);
|
||||
assertTrue(randomRecipient.isDestroyed());
|
||||
|
||||
System.out.println("...terminalFailures=2");
|
||||
System.out.println("terminalLimitAndRandomFailuresDestroyPasswordRecipients...ok");
|
||||
}
|
||||
|
||||
private static MultiRecipientDataSourceBuilder builderWithAes() {
|
||||
ZeroEchoSession session = new ZeroEchoSession().withPbkdf2Limits(LIMITS);
|
||||
return MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Recipient> recipients(MultiRecipientDataSourceBuilder builder) throws Exception {
|
||||
Field field = MultiRecipientDataSourceBuilder.class.getDeclaredField("recipients");
|
||||
field.setAccessible(true);
|
||||
return (List<Recipient>) field.get(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.guard;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.io.Util;
|
||||
import zeroecho.sdk.Pbkdf2Limits;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
|
||||
class SessionRecipientOpenerContractTest {
|
||||
|
||||
@Test
|
||||
void publicApiExposesOnlyReusableSessionOpeners() {
|
||||
System.out.println("publicApiExposesOnlyReusableSessionOpeners");
|
||||
assertSessionOnlyConstructor(KemCtxOpener.class);
|
||||
assertSessionOnlyConstructor(EncCtxOpener.class);
|
||||
|
||||
List<Method> addOpenerMethods = Arrays.stream(MultiRecipientDataSourceBuilder.class.getMethods())
|
||||
.filter(method -> method.getName().equals("addOpener"))
|
||||
.toList();
|
||||
assertEquals(1, addOpenerMethods.size());
|
||||
assertEquals(RecipientOpener.class, addOpenerMethods.get(0).getParameterTypes()[0]);
|
||||
|
||||
System.out.println("...addOpenerOverloads=1");
|
||||
System.out.println("publicApiExposesOnlyReusableSessionOpeners...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionOpenersIgnoreUnrelatedEntryFamilies() throws Exception {
|
||||
System.out.println("sessionOpenersIgnoreUnrelatedEntryFamilies");
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
UnlockMaterial.Password material = new UnlockMaterial.Password(new char[] { 'p' });
|
||||
try {
|
||||
assertNull(new KemCtxOpener(session).tryOpen(
|
||||
"CTX-ENC:RSA", new byte[0], material));
|
||||
assertNull(new EncCtxOpener(session).tryOpen(
|
||||
"KEM:ML-KEM:GCM-WRAP", new byte[0], material));
|
||||
} finally {
|
||||
material.destroy();
|
||||
}
|
||||
System.out.println("...ignoredFamilies=2");
|
||||
System.out.println("sessionOpenersIgnoreUnrelatedEntryFamilies...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customReusableOpenerScansEveryEntryAndClosesOnce() throws Exception {
|
||||
System.out.println("customReusableOpenerScansEveryEntryAndClosesOnce");
|
||||
ZeroEchoSession session = new ZeroEchoSession()
|
||||
.withPbkdf2Limits(new Pbkdf2Limits(20_000, 30_000));
|
||||
TrackingOpener opener = new TrackingOpener();
|
||||
UnlockMaterial.Password material = new UnlockMaterial.Password(new char[] { 'p' });
|
||||
try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
|
||||
.unlockWith(material)
|
||||
.addOpener(opener);
|
||||
MultiRecipientContent content = builder.build(false)) {
|
||||
content.setInput(() -> new ByteArrayInputStream(twoEntryHeader()));
|
||||
assertThrows(IOException.class, content::getStream);
|
||||
} finally {
|
||||
material.destroy();
|
||||
}
|
||||
|
||||
assertEquals(2, opener.attempts);
|
||||
assertEquals(1, opener.closeCount);
|
||||
System.out.println("...attempts=" + opener.attempts);
|
||||
System.out.println("customReusableOpenerScansEveryEntryAndClosesOnce...ok");
|
||||
}
|
||||
|
||||
private static void assertSessionOnlyConstructor(Class<?> openerClass) {
|
||||
Constructor<?>[] constructors = openerClass.getConstructors();
|
||||
assertEquals(1, constructors.length);
|
||||
assertEquals(ZeroEchoSession.class, constructors[0].getParameterTypes()[0]);
|
||||
}
|
||||
|
||||
private static byte[] twoEntryHeader() throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
Util.writePack7I(output, 2);
|
||||
Util.writeUTF8(output, "first");
|
||||
Util.write(output, new byte[] { 1 });
|
||||
Util.writeUTF8(output, "second");
|
||||
Util.write(output, new byte[] { 2 });
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private static final class TrackingOpener implements RecipientOpener {
|
||||
private int attempts;
|
||||
private int closeCount;
|
||||
|
||||
@Override
|
||||
public byte[] tryOpen(String entryId, byte[] entryBlob, UnlockMaterial material) {
|
||||
attempts++;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ public class HybridDerivedTest {
|
||||
byte[] aad = "aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] msg = fixedBytes(1024, (byte) 0x5A);
|
||||
|
||||
AesDataContentBuilder encAes = AesDataContentBuilder.builder().withHeader().modeGcm(128);
|
||||
AesDataContentBuilder encAes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader().modeGcm(128);
|
||||
|
||||
AesDataContentBuilder returnedEnc = HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript)
|
||||
.aad(aad).applyToAesGcm(encAes, 256, 12);
|
||||
@@ -88,7 +88,7 @@ public class HybridDerivedTest {
|
||||
System.out.println("...ciphertextLen=" + ciphertext.length);
|
||||
System.out.println("...ciphertextPrefix=" + shortHex(ciphertext, 32));
|
||||
|
||||
AesDataContentBuilder decAes = AesDataContentBuilder.builder().withHeader().modeGcm(128);
|
||||
AesDataContentBuilder decAes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader().modeGcm(128);
|
||||
|
||||
HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript).aad(aad).applyToAesGcm(decAes, 256,
|
||||
12);
|
||||
@@ -109,7 +109,7 @@ public class HybridDerivedTest {
|
||||
byte[] aad = "aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] msg = fixedBytes(256, (byte) 0x1C);
|
||||
|
||||
AesDataContentBuilder encAes = AesDataContentBuilder.builder().withHeader().modeGcm(128);
|
||||
AesDataContentBuilder encAes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader().modeGcm(128);
|
||||
|
||||
HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript).aad(aad).applyToAesGcm(encAes, 256,
|
||||
12);
|
||||
@@ -117,7 +117,7 @@ public class HybridDerivedTest {
|
||||
byte[] ciphertext = runEncrypt(encAes, msg);
|
||||
System.out.println("...ciphertextLen=" + ciphertext.length);
|
||||
|
||||
AesDataContentBuilder decAesWrong = AesDataContentBuilder.builder().withHeader().modeGcm(128);
|
||||
AesDataContentBuilder decAesWrong = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader().modeGcm(128);
|
||||
|
||||
// ...label mismatch -> wrong key/iv/aad -> decryption must fail
|
||||
HybridDerived.from(exporter).label("app/enc/aes_WRONG").transcript(transcript).aad(aad)
|
||||
@@ -137,7 +137,7 @@ public class HybridDerivedTest {
|
||||
byte[] aad = "aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] msg = fixedBytes(777, (byte) 0x33);
|
||||
|
||||
ChaChaDataContentBuilder encChaCha = ChaChaDataContentBuilder.builder().withHeader();
|
||||
ChaChaDataContentBuilder encChaCha = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader();
|
||||
|
||||
ChaChaDataContentBuilder returnedEnc = HybridDerived.from(exporter).label("app/enc/chacha")
|
||||
.transcript(transcript).aad(aad).applyToChaChaAead(encChaCha, 256, 12);
|
||||
@@ -149,7 +149,7 @@ public class HybridDerivedTest {
|
||||
System.out.println("...ciphertextLen=" + ciphertext.length);
|
||||
System.out.println("...ciphertextPrefix=" + shortHex(ciphertext, 32));
|
||||
|
||||
ChaChaDataContentBuilder decChaCha = ChaChaDataContentBuilder.builder().withHeader();
|
||||
ChaChaDataContentBuilder decChaCha = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader();
|
||||
|
||||
HybridDerived.from(exporter).label("app/enc/chacha").transcript(transcript).aad(aad)
|
||||
.applyToChaChaAead(decChaCha, 256, 12);
|
||||
@@ -174,7 +174,7 @@ public class HybridDerivedTest {
|
||||
// recommended bits
|
||||
// --------------------
|
||||
|
||||
HmacDataContentBuilder macBuilder = HmacDataContentBuilder.builder().sha256().emitHexTag();
|
||||
HmacDataContentBuilder macBuilder = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256().emitHexTag();
|
||||
|
||||
int recommendedBits = macBuilder.recommendedKeyBits();
|
||||
System.out.println("...recommendedBits=" + recommendedBits);
|
||||
@@ -184,7 +184,7 @@ public class HybridDerivedTest {
|
||||
String tagHex = runHmacHex(macBuilder, msg);
|
||||
System.out.println("...tagHexPrefix=" + shortText(tagHex, 64));
|
||||
|
||||
HmacDataContentBuilder verifyBuilder = HmacDataContentBuilder.builder().sha256().expectedTagHex(tagHex)
|
||||
HmacDataContentBuilder verifyBuilder = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256().expectedTagHex(tagHex)
|
||||
.emitVerificationBoolean();
|
||||
|
||||
HybridDerived.from(exporter).label("app/mac/hmac-default").transcript(transcript).applyToHmac(verifyBuilder);
|
||||
@@ -197,7 +197,7 @@ public class HybridDerivedTest {
|
||||
// Override key size path: applyToHmac(hmac, keyBits)
|
||||
// --------------------
|
||||
|
||||
HmacDataContentBuilder macBuilderOv = HmacDataContentBuilder.builder().sha256().emitHexTag();
|
||||
HmacDataContentBuilder macBuilderOv = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256().emitHexTag();
|
||||
|
||||
// ...override to 512-bit keying material (still valid for HMAC; explicit expert
|
||||
// choice)
|
||||
@@ -207,7 +207,7 @@ public class HybridDerivedTest {
|
||||
String tagHexOv = runHmacHex(macBuilderOv, msg);
|
||||
System.out.println("...tagHexOvPrefix=" + shortText(tagHexOv, 64));
|
||||
|
||||
HmacDataContentBuilder verifyBuilderOv = HmacDataContentBuilder.builder().sha256().expectedTagHex(tagHexOv)
|
||||
HmacDataContentBuilder verifyBuilderOv = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256().expectedTagHex(tagHexOv)
|
||||
.emitVerificationBoolean();
|
||||
|
||||
HybridDerived.from(exporter).label("app/mac/hmac-override").transcript(transcript).applyToHmac(verifyBuilderOv,
|
||||
@@ -221,7 +221,7 @@ public class HybridDerivedTest {
|
||||
// Negative: wrong expected tag -> must emit "false"
|
||||
// --------------------
|
||||
|
||||
HmacDataContentBuilder verifyBad = HmacDataContentBuilder.builder().sha256()
|
||||
HmacDataContentBuilder verifyBad = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256()
|
||||
.expectedTagHex(tagHex.substring(0, Math.max(0, tagHex.length() - 2)) + "00").emitVerificationBoolean();
|
||||
|
||||
HybridDerived.from(exporter).label("app/mac/hmac-default").transcript(transcript).applyToHmac(verifyBad);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.hybrid.kex;
|
||||
|
||||
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 static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HybridKexExporterLifecycleTest {
|
||||
|
||||
@Test
|
||||
void constructorOwnsDefensiveCopies() throws Exception {
|
||||
System.out.println("constructorOwnsDefensiveCopies");
|
||||
byte[] root = filled(32, (byte) 1);
|
||||
byte[] salt = filled(16, (byte) 2);
|
||||
HybridKexExporter exporter = new HybridKexExporter(root, salt);
|
||||
Arrays.fill(root, (byte) 0);
|
||||
Arrays.fill(salt, (byte) 0);
|
||||
|
||||
assertFalse(Arrays.equals(root, exporter.rootSecretCopy()));
|
||||
assertArrayEquals(filled(32, (byte) 1), internal(exporter, "rootSecret"));
|
||||
assertArrayEquals(filled(16, (byte) 2), internal(exporter, "salt"));
|
||||
exporter.close();
|
||||
System.out.println("...ownedCopies=true");
|
||||
System.out.println("constructorOwnsDefensiveCopies...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void destructionClearsSecretsAndRejectsFurtherUse() throws Exception {
|
||||
System.out.println("destructionClearsSecretsAndRejectsFurtherUse");
|
||||
HybridKexExporter exporter = new HybridKexExporter(filled(32, (byte) 3), filled(16, (byte) 4));
|
||||
byte[] internalRoot = internal(exporter, "rootSecret");
|
||||
byte[] internalSalt = internal(exporter, "salt");
|
||||
|
||||
exporter.destroy();
|
||||
exporter.destroy();
|
||||
exporter.close();
|
||||
|
||||
assertTrue(exporter.isDestroyed());
|
||||
assertArrayEquals(new byte[internalRoot.length], internalRoot);
|
||||
assertArrayEquals(new byte[internalSalt.length], internalSalt);
|
||||
assertThrows(IllegalStateException.class, () -> exporter.export("label", null, 16));
|
||||
assertThrows(IllegalStateException.class, exporter::rootSecretCopy);
|
||||
System.out.println("...destroyed=true...postDestroyRejected=true");
|
||||
System.out.println("destructionClearsSecretsAndRejectsFurtherUse...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulExportDoesNotDestroyExporter() {
|
||||
System.out.println("successfulExportDoesNotDestroyExporter");
|
||||
HybridKexExporter exporter = new HybridKexExporter(filled(32, (byte) 5), null);
|
||||
|
||||
byte[] first = exporter.export("first", null, 16);
|
||||
byte[] second = exporter.export("second", new byte[] { 1 }, 16);
|
||||
|
||||
assertFalse(exporter.isDestroyed());
|
||||
assertFalse(Arrays.equals(first, second));
|
||||
exporter.close();
|
||||
Arrays.fill(first, (byte) 0);
|
||||
Arrays.fill(second, (byte) 0);
|
||||
System.out.println("...derivedLengths=16,16");
|
||||
System.out.println("successfulExportDoesNotDestroyExporter...ok");
|
||||
}
|
||||
|
||||
private static byte[] internal(HybridKexExporter exporter, String fieldName) throws Exception {
|
||||
Field field = HybridKexExporter.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
return (byte[]) field.get(exporter);
|
||||
}
|
||||
|
||||
private static byte[] filled(int length, byte value) {
|
||||
byte[] result = new byte[length];
|
||||
Arrays.fill(result, value);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.hybrid.kex;
|
||||
|
||||
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.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
|
||||
/**
|
||||
* Verifies provider-independent hybrid frame validation and size boundaries.
|
||||
*/
|
||||
class HybridKexFrameCodecTest {
|
||||
|
||||
@Test
|
||||
void acceptsZeroLengthComponents() throws Exception {
|
||||
String name = start("acceptsZeroLengthComponents");
|
||||
byte[] frame = HybridKexContext.encode(new byte[0], new byte[0]);
|
||||
HybridKexContext.Parts parts = HybridKexContext.decode(frame);
|
||||
|
||||
assertEquals(8, frame.length);
|
||||
assertArrayEquals(new byte[0], parts.classicPart());
|
||||
assertArrayEquals(new byte[0], parts.pqcPart());
|
||||
progress("frameBytes=" + frame.length);
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsInclusiveMaximumFrame() throws Exception {
|
||||
String name = start("acceptsInclusiveMaximumFrame");
|
||||
byte[] classic = new byte[HybridKexContext.MAX_FRAME_BYTES - 8];
|
||||
byte[] frame = HybridKexContext.encode(classic, new byte[0]);
|
||||
|
||||
assertEquals(HybridKexContext.MAX_FRAME_BYTES, frame.length);
|
||||
assertEquals(classic.length, HybridKexContext.decode(frame).classicPart().length);
|
||||
progress("frameBytes=" + frame.length);
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOversizedEncoding() {
|
||||
String name = start("rejectsOversizedEncoding");
|
||||
byte[] classic = new byte[HybridKexContext.MAX_FRAME_BYTES - 7];
|
||||
|
||||
assertThrows(IOException.class, () -> HybridKexContext.encode(classic, new byte[0]));
|
||||
progress("classicBytes=" + classic.length);
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMalformedFrames() {
|
||||
String name = start("rejectsMalformedFrames");
|
||||
byte[][] malformed = {
|
||||
new byte[7],
|
||||
ints(-1, 0),
|
||||
ints(0, -1),
|
||||
ints(1, 0),
|
||||
ints(0, 1),
|
||||
append(ints(0, 0), (byte) 0x7f),
|
||||
new byte[HybridKexContext.MAX_FRAME_BYTES + 1],
|
||||
ints(0, HybridKexContext.MAX_FRAME_BYTES),
|
||||
ints(Integer.MAX_VALUE, 0)
|
||||
};
|
||||
|
||||
for (byte[] frame : malformed) {
|
||||
assertThrows(IOException.class, () -> HybridKexContext.decode(frame));
|
||||
}
|
||||
progress("cases=" + malformed.length);
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicApiMapsMalformedFrame() {
|
||||
String name = start("publicApiMapsMalformedFrame");
|
||||
AgreementContext classic = mock(AgreementContext.class);
|
||||
MessageAgreementContext pqc = mock(MessageAgreementContext.class);
|
||||
HybridKexContext context = new HybridKexContext(HybridKexProfile.defaultProfile(32), classic, pqc);
|
||||
|
||||
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
|
||||
() -> context.setPeerMessage(ints(Integer.MAX_VALUE, 0)));
|
||||
|
||||
assertInstanceOf(IOException.class, failure.getCause());
|
||||
verifyNoInteractions(classic, pqc);
|
||||
progress("mapped=IllegalArgument");
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearsComponentSecretsAfterSuccessfulDerivation() {
|
||||
String name = start("clearsComponentSecretsAfterSuccessfulDerivation");
|
||||
byte[] classicSecret = { 1, 2, 3 };
|
||||
byte[] pqcSecret = { 4, 5, 6 };
|
||||
AgreementContext classic = mock(AgreementContext.class);
|
||||
MessageAgreementContext pqc = mock(MessageAgreementContext.class);
|
||||
when(classic.deriveSecret()).thenReturn(classicSecret);
|
||||
when(pqc.deriveSecret()).thenReturn(pqcSecret);
|
||||
HybridKexContext context = new HybridKexContext(HybridKexProfile.defaultProfile(16), classic, pqc);
|
||||
|
||||
byte[] result = context.deriveSecret();
|
||||
|
||||
assertEquals(16, result.length);
|
||||
assertZeroized(classicSecret);
|
||||
assertZeroized(pqcSecret);
|
||||
progress("resultBytes=" + result.length);
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearsClassicSecretWhenSecondLegFails() {
|
||||
String name = start("clearsClassicSecretWhenSecondLegFails");
|
||||
byte[] classicSecret = { 1, 2, 3 };
|
||||
IllegalStateException expected = new IllegalStateException("controlled second-leg failure");
|
||||
AgreementContext classic = mock(AgreementContext.class);
|
||||
MessageAgreementContext pqc = mock(MessageAgreementContext.class);
|
||||
when(classic.deriveSecret()).thenReturn(classicSecret);
|
||||
when(pqc.deriveSecret()).thenThrow(expected);
|
||||
HybridKexContext context = new HybridKexContext(HybridKexProfile.defaultProfile(16), classic, pqc);
|
||||
|
||||
assertSame(expected, assertThrows(IllegalStateException.class, context::deriveSecret));
|
||||
assertZeroized(classicSecret);
|
||||
progress("failure=secondLeg");
|
||||
ok(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearsAllTemporarySecretsWhenKdfFails() {
|
||||
String name = start("clearsAllTemporarySecretsWhenKdfFails");
|
||||
byte[] classicSecret = { 1, 2, 3 };
|
||||
byte[] pqcSecret = { 4, 5, 6 };
|
||||
byte[][] observedInput = new byte[1][];
|
||||
AgreementContext classic = mock(AgreementContext.class);
|
||||
MessageAgreementContext pqc = mock(MessageAgreementContext.class);
|
||||
when(classic.deriveSecret()).thenReturn(classicSecret);
|
||||
when(pqc.deriveSecret()).thenReturn(pqcSecret);
|
||||
HybridKexContext context = new HybridKexContext(HybridKexProfile.defaultProfile(16), classic, pqc,
|
||||
(ikm, salt, info, outputLength) -> {
|
||||
observedInput[0] = ikm;
|
||||
throw new GeneralSecurityException("controlled KDF failure");
|
||||
});
|
||||
|
||||
IllegalStateException failure = assertThrows(IllegalStateException.class, context::deriveSecret);
|
||||
|
||||
assertInstanceOf(GeneralSecurityException.class, failure.getCause());
|
||||
assertZeroized(classicSecret);
|
||||
assertZeroized(pqcSecret);
|
||||
assertZeroized(observedInput[0]);
|
||||
progress("failure=KDF");
|
||||
ok(name);
|
||||
}
|
||||
|
||||
private static byte[] ints(int first, int second) {
|
||||
return ByteBuffer.allocate(8).putInt(first).putInt(second).array();
|
||||
}
|
||||
|
||||
private static byte[] append(byte[] input, byte value) {
|
||||
byte[] output = new byte[input.length + 1];
|
||||
System.arraycopy(input, 0, output, 0, input.length);
|
||||
output[input.length] = value;
|
||||
return output;
|
||||
}
|
||||
|
||||
private static void assertZeroized(byte[] value) {
|
||||
assertTrue(value != null && Arrays.equals(new byte[value.length], value));
|
||||
}
|
||||
|
||||
private static String start(String routine) {
|
||||
String label = routine.length() <= 30 ? routine : routine.substring(0, 27) + "...";
|
||||
System.out.println(label);
|
||||
return label;
|
||||
}
|
||||
|
||||
private static void progress(String detail) {
|
||||
System.out.println("..." + detail);
|
||||
}
|
||||
|
||||
private static void ok(String name) {
|
||||
System.out.println(name + "...ok");
|
||||
}
|
||||
}
|
||||
@@ -129,11 +129,11 @@ public class HybridKexTest {
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
// Classic: X25519 key pairs (Xdh + XdhSpec.X25519)
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// PQC: ML-KEM key pair (Kyber variant)
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
HybridKexContext alice = null;
|
||||
HybridKexContext bob = null;
|
||||
@@ -141,12 +141,12 @@ public class HybridKexTest {
|
||||
try {
|
||||
// Initiator: classic uses Alice private + Bob classic public; PQC uses Bob PQC
|
||||
// public
|
||||
alice = HybridKexContexts.initiator(profile, "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(),
|
||||
alice = HybridKexContexts.initiator(new zeroecho.sdk.ZeroEchoSession(), profile, "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(),
|
||||
XdhSpec.X25519, "ML-KEM", bobPqc.getPublic(), null);
|
||||
|
||||
// Responder: classic uses Bob private + Alice classic public; PQC uses Bob PQC
|
||||
// private
|
||||
bob = HybridKexContexts.responder(profile, "Xdh", bobClassic.getPrivate(), aliceClassic.getPublic(),
|
||||
bob = HybridKexContexts.responder(new zeroecho.sdk.ZeroEchoSession(), profile, "Xdh", bobClassic.getPrivate(), aliceClassic.getPublic(),
|
||||
XdhSpec.X25519, "ML-KEM", bobPqc.getPrivate(), null);
|
||||
|
||||
// Alice produces message (contains PQC ciphertext; classic part is empty here)
|
||||
@@ -190,11 +190,11 @@ public class HybridKexTest {
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
// Classic: X25519 key pairs (Xdh + XdhSpec.X25519)
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// PQC: ML-KEM key pair (recipient/responder)
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
HybridKexContext alice = null;
|
||||
HybridKexContext bob = null;
|
||||
@@ -204,10 +204,10 @@ public class HybridKexTest {
|
||||
// KeyPairKey + ContextSpec).
|
||||
// PQC leg is KEM-style: initiator uses recipient public key; responder uses
|
||||
// recipient private key.
|
||||
alice = HybridKexContexts.initiatorPairMessage(profile, "Xdh", new KeyPairKey(aliceClassic), XdhSpec.X25519,
|
||||
alice = HybridKexContexts.initiatorPairMessage(new zeroecho.sdk.ZeroEchoSession(), profile, "Xdh", new KeyPairKey(aliceClassic), XdhSpec.X25519,
|
||||
"ML-KEM", bobPqc.getPublic(), null);
|
||||
|
||||
bob = HybridKexContexts.responderPairMessage(profile, "Xdh", new KeyPairKey(bobClassic), XdhSpec.X25519,
|
||||
bob = HybridKexContexts.responderPairMessage(new zeroecho.sdk.ZeroEchoSession(), profile, "Xdh", new KeyPairKey(bobClassic), XdhSpec.X25519,
|
||||
"ML-KEM", bobPqc.getPrivate(), null);
|
||||
|
||||
// Step 1: Alice -> Bob (classic SPKI + PQC ciphertext)
|
||||
|
||||
@@ -53,10 +53,14 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec;
|
||||
import zeroecho.core.alg.rsa.RsaKeyGenSpec;
|
||||
import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.io.TailStrippingInputStream;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.builders.core.DataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentChainBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
@@ -167,7 +171,7 @@ public class HybridSignatureTest {
|
||||
}
|
||||
|
||||
private static int tagLen(String algoId, KeyUsage role, Key key, ContextSpec specOrNull) throws Exception {
|
||||
try (SignatureContext ctx = CryptoAlgorithms.create(algoId, role, key, specOrNull)) {
|
||||
try (SignatureContext ctx = new zeroecho.sdk.ZeroEchoSession().createContext(algoId, role, key, specOrNull)) {
|
||||
return ctx.tagLength();
|
||||
}
|
||||
}
|
||||
@@ -237,8 +241,11 @@ public class HybridSignatureTest {
|
||||
byte[] msg = randomBytes(size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
KeyPair ed = session.keyBuilders().asymmetric().generateKeyPair("Ed25519",
|
||||
Ed25519KeyGenSpec.defaultSpec());
|
||||
KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+",
|
||||
SphincsPlusKeyGenSpec.defaultSpec());
|
||||
|
||||
int edLen = tagLen("Ed25519", KeyUsage.SIGN, ed.getPrivate(), null);
|
||||
int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null);
|
||||
@@ -249,14 +256,14 @@ public class HybridSignatureTest {
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
|
||||
byte[] sigAnd;
|
||||
try (SignatureContext signer = HybridSignatureContexts.sign(andProfile, ed.getPrivate(), spx.getPrivate(),
|
||||
try (SignatureContext signer = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), andProfile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
sigAnd = signTrailer(signer, msg);
|
||||
}
|
||||
System.out.println("...sig(AND).len=" + sigAnd.length + ", head=" + hexShort(sigAnd));
|
||||
|
||||
// verify OK
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), andProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(sigAnd);
|
||||
@@ -268,7 +275,7 @@ public class HybridSignatureTest {
|
||||
|
||||
// corrupt classic => must fail
|
||||
byte[] badClassic = concat(flipOneBit(sub(sigAnd, 0, edLen), 0), sub(sigAnd, edLen, spxLen));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), andProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(badClassic);
|
||||
@@ -282,7 +289,7 @@ public class HybridSignatureTest {
|
||||
|
||||
// corrupt pqc => must fail
|
||||
byte[] badPqc = concat(sub(sigAnd, 0, edLen), flipOneBit(sub(sigAnd, edLen, spxLen), 0));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), andProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(badPqc);
|
||||
@@ -299,7 +306,7 @@ public class HybridSignatureTest {
|
||||
HybridSignatureProfile.VerifyRule.OR);
|
||||
|
||||
byte[] sigOr;
|
||||
try (SignatureContext signer = HybridSignatureContexts.sign(orProfile, ed.getPrivate(), spx.getPrivate(),
|
||||
try (SignatureContext signer = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), orProfile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
sigOr = signTrailer(signer, msg);
|
||||
}
|
||||
@@ -307,7 +314,7 @@ public class HybridSignatureTest {
|
||||
|
||||
// corrupt classic => OR must pass
|
||||
byte[] orBadClassic = concat(flipOneBit(sub(sigOr, 0, edLen), 0), sub(sigOr, edLen, spxLen));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), orProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(orBadClassic);
|
||||
@@ -319,7 +326,7 @@ public class HybridSignatureTest {
|
||||
|
||||
// corrupt pqc => OR must pass
|
||||
byte[] orBadPqc = concat(sub(sigOr, 0, edLen), flipOneBit(sub(sigOr, edLen, spxLen), 0));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), orProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(orBadPqc);
|
||||
@@ -331,7 +338,7 @@ public class HybridSignatureTest {
|
||||
|
||||
// corrupt both => OR must fail
|
||||
byte[] orBadBoth = concat(flipOneBit(sub(sigOr, 0, edLen), 0), flipOneBit(sub(sigOr, edLen, spxLen), 0));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), orProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(orBadBoth);
|
||||
@@ -357,8 +364,10 @@ public class HybridSignatureTest {
|
||||
byte[] msg = randomBytes(size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair rsa = CryptoAlgorithms.require("RSA").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
KeyPair rsa = session.keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+",
|
||||
SphincsPlusKeyGenSpec.defaultSpec());
|
||||
|
||||
int rsaLen = tagLen("RSA", KeyUsage.SIGN, rsa.getPrivate(), null);
|
||||
int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null);
|
||||
@@ -368,13 +377,13 @@ public class HybridSignatureTest {
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
|
||||
byte[] sig;
|
||||
try (SignatureContext signer = HybridSignatureContexts.sign(profile, rsa.getPrivate(), spx.getPrivate(),
|
||||
try (SignatureContext signer = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
sig = signTrailer(signer, msg);
|
||||
}
|
||||
System.out.println("...sig.len=" + sig.length + ", head=" + hexShort(sig));
|
||||
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(sig);
|
||||
@@ -386,7 +395,7 @@ public class HybridSignatureTest {
|
||||
|
||||
// negative sanity: corrupt classic => must fail (AND)
|
||||
byte[] badClassic = concat(flipOneBit(sub(sig, 0, rsaLen), 0), sub(sig, rsaLen, spxLen));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(badClassic);
|
||||
@@ -416,8 +425,11 @@ public class HybridSignatureTest {
|
||||
byte[] msg = randomBytes(size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
KeyPair ed = session.keyBuilders().asymmetric().generateKeyPair("Ed25519",
|
||||
Ed25519KeyGenSpec.defaultSpec());
|
||||
KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+",
|
||||
SphincsPlusKeyGenSpec.defaultSpec());
|
||||
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
@@ -425,7 +437,7 @@ public class HybridSignatureTest {
|
||||
byte[] out;
|
||||
int tagLen;
|
||||
|
||||
try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
|
||||
try (SignatureContext tagEnc = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
|
||||
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
|
||||
@@ -437,7 +449,7 @@ public class HybridSignatureTest {
|
||||
|
||||
System.out.println("...out=" + out.length + " bytes");
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
@@ -468,8 +480,11 @@ public class HybridSignatureTest {
|
||||
msg = Arrays.copyOf(msg, size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
KeyPair ed = session.keyBuilders().asymmetric().generateKeyPair("Ed25519",
|
||||
Ed25519KeyGenSpec.defaultSpec());
|
||||
KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+",
|
||||
SphincsPlusKeyGenSpec.defaultSpec());
|
||||
|
||||
int edLen = tagLen("Ed25519", KeyUsage.SIGN, ed.getPrivate(), null);
|
||||
int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null);
|
||||
@@ -481,7 +496,7 @@ public class HybridSignatureTest {
|
||||
byte[] out;
|
||||
int tagLen;
|
||||
|
||||
try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
|
||||
try (SignatureContext tagEnc = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
|
||||
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
|
||||
@@ -500,7 +515,7 @@ public class HybridSignatureTest {
|
||||
byte[] badClassic = concat(flipOneBit(sub(tag, 0, edLen), 0), sub(tag, edLen, spxLen));
|
||||
byte[] outBadClassic = concat(body, badClassic);
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
@@ -518,7 +533,7 @@ public class HybridSignatureTest {
|
||||
byte[] badPqc = concat(sub(tag, 0, edLen), flipOneBit(sub(tag, edLen, spxLen), 0));
|
||||
byte[] outBadPqc = concat(body, badPqc);
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
@@ -536,7 +551,7 @@ public class HybridSignatureTest {
|
||||
byte[] badBoth = concat(flipOneBit(sub(tag, 0, edLen), 0), flipOneBit(sub(tag, edLen, spxLen), 0));
|
||||
byte[] outBadBoth = concat(body, badBoth);
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
@@ -564,15 +579,17 @@ public class HybridSignatureTest {
|
||||
byte[] msg = randomBytes(size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair rsa = CryptoAlgorithms.require("RSA").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
KeyPair rsa = session.keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048());
|
||||
KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+",
|
||||
SphincsPlusKeyGenSpec.defaultSpec());
|
||||
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile("RSA", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
|
||||
byte[] out;
|
||||
|
||||
try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, rsa.getPrivate(), spx.getPrivate(),
|
||||
try (SignatureContext tagEnc = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
|
||||
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
|
||||
@@ -583,7 +600,7 @@ public class HybridSignatureTest {
|
||||
|
||||
System.out.println("...out=" + out.length + " bytes");
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(),
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
|
||||
69
lib/src/test/java/zeroecho/sdk/util/PasswordTest.java
Normal file
69
lib/src/test/java/zeroecho/sdk/util/PasswordTest.java
Normal file
@@ -0,0 +1,69 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PasswordTest {
|
||||
@Test
|
||||
void canonicalRandomFacadeValidatesNullAndAcceptsEmptyArrays() {
|
||||
System.out.print("Password/random-boundaries...");
|
||||
assertThrows(NullPointerException.class, () -> Password.generateRandom(null));
|
||||
byte[] empty = new byte[0];
|
||||
assertSame(empty, Password.generateRandom(empty));
|
||||
assertThrows(IllegalArgumentException.class, () -> Password.generatePrintablePasswordChars(0));
|
||||
assertThrows(IllegalArgumentException.class, () -> Password.generatePrintablePasswordChars(-1));
|
||||
|
||||
char[] printable = Password.generatePrintablePasswordChars(128);
|
||||
assertEquals(128, printable.length);
|
||||
for (char character : printable) {
|
||||
assertTrue(character >= '!' && character <= '~');
|
||||
}
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sharedSourceSupportsParallelUse() throws Exception {
|
||||
System.out.print("RandomSupport/concurrent...");
|
||||
SecureRandom source = RandomSupport.getRandom();
|
||||
assertSame(source, RandomSupport.getRandom());
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
List<Future<byte[]>> futures = new ArrayList<>();
|
||||
for (int i = 0; i < 64; i++) {
|
||||
futures.add(executor.submit(() -> {
|
||||
assertSame(source, RandomSupport.getRandom());
|
||||
return Password.generateRandom(new byte[32]);
|
||||
}));
|
||||
}
|
||||
byte[] first = futures.get(0).get();
|
||||
boolean differentOutputObserved = false;
|
||||
for (int i = 1; i < futures.size(); i++) {
|
||||
byte[] output = futures.get(i).get();
|
||||
if (!java.util.Arrays.equals(first, output)) {
|
||||
differentOutputObserved = true;
|
||||
}
|
||||
}
|
||||
assertTrue(differentOutputObserved);
|
||||
assertNotEquals(0, first.length);
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
System.out.println("ok");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user