diff --git a/app/src/main/java/zeroecho/Guard.java b/app/src/main/java/zeroecho/Guard.java
index 35ec7e9..aece07b 100644
--- a/app/src/main/java/zeroecho/Guard.java
+++ b/app/src/main/java/zeroecho/Guard.java
@@ -154,7 +154,7 @@ public final class Guard {
final Option OPT_TAG_BITS = Option.builder().longOpt("tag-bits").hasArg().argName("96..128")
.desc("AES-GCM tag length in bits (default 128)").get();
final Option OPT_NONCE_HEX = Option.builder().longOpt("nonce-hex").hasArg().argName("hex")
- .desc("ChaCha nonce (12-byte hex)").get();
+ .desc("ChaCha decryption nonce (12-byte hex; rejected for encryption)").get();
final Option OPT_INIT_CTR = Option.builder().longOpt("init-ctr").hasArg().argName("int")
.desc("ChaCha stream initial counter (default 1)").get();
final Option OPT_CTR = Option.builder().longOpt("ctr").hasArg().argName("int")
@@ -303,7 +303,7 @@ public final class Guard {
chacha.withHeader();
}
if (chachaNonce != null) {
- chacha.withNonce(chachaNonce);
+ chacha.withDecryptionNonce(chachaNonce);
}
if (ctrOverride != null || initCtr != null) {
// providing counters together with AAD would be conflicting; builder enforces
@@ -323,7 +323,7 @@ public final class Guard {
chacha.withHeader();
}
if (chachaNonce != null) {
- chacha.withNonce(chachaNonce);
+ chacha.withDecryptionNonce(chachaNonce);
}
if (initCtr != null) {
chacha.initialCounter(initCtr);
diff --git a/app/src/main/java/zeroecho/Kem.java b/app/src/main/java/zeroecho/Kem.java
index 4ed2b24..bbc3629 100644
--- a/app/src/main/java/zeroecho/Kem.java
+++ b/app/src/main/java/zeroecho/Kem.java
@@ -181,7 +181,7 @@ public final class Kem { // NOPMD
/** AES IV: --aes-iv <hex> */
public static final Option OPT_AES_IV = Option.builder().longOpt("aes-iv").hasArg().argName("hex")
- .desc("AES IV/nonce (hex)").get();
+ .desc("AES decryption IV/nonce (hex; rejected for encryption)").get();
/** AES tag bits: --aes-tag-bits <int> */
public static final Option OPT_AES_TAG_BITS = Option.builder().longOpt("aes-tag-bits").hasArg().argName("int")
@@ -189,7 +189,7 @@ public final class Kem { // NOPMD
/** ChaCha nonce: --chacha-nonce <hex> */
public static final Option OPT_CHACHA_NONCE = Option.builder().longOpt("chacha-nonce").hasArg().argName("hex")
- .desc("ChaCha nonce (hex, usually 12 bytes)").get();
+ .desc("ChaCha decryption nonce (hex, usually 12 bytes; rejected for encryption)").get();
/** ChaCha counter value: --chacha-counter <int> */
public static final Option OPT_CHACHA_COUNTER = Option.builder().longOpt("chacha-counter").hasArg().argName("int")
@@ -283,7 +283,7 @@ public final class Kem { // NOPMD
}
byte[] iv = parseHexOpt(cmd, OPT_AES_IV);
if (iv != null) {
- aes = aes.withIv(iv);
+ aes = aes.withDecryptionIv(iv);
}
if (aad != null && aad.length > 0) {
aes = aes.withAad(aad);
@@ -299,7 +299,7 @@ public final class Kem { // NOPMD
ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder(session);
byte[] nonce = parseHexOpt(cmd, OPT_CHACHA_NONCE);
if (nonce != null) {
- cc = cc.withNonce(nonce);
+ cc = cc.withDecryptionNonce(nonce);
}
// counter is an integer, not bytes; use typed parsed option
Integer counter = parsedIntOpt(cmd, OPT_CHACHA_COUNTER);
diff --git a/app/src/test/java/zeroecho/GuardTest.java b/app/src/test/java/zeroecho/GuardTest.java
index 59436aa..c5a2da2 100644
--- a/app/src/test/java/zeroecho/GuardTest.java
+++ b/app/src/test/java/zeroecho/GuardTest.java
@@ -196,11 +196,10 @@ public class GuardTest {
final int tagBits = 128;
final String aadAes = "010203";
final String aadCha = "D00DFEED";
- final String chNonce = "00112233445566778899AABB";
System.out.println(method);
System.out.println("...params: sizeAes=" + sizeAes + " sizeCha=" + sizeCha + " tagBits=" + tagBits + " aadAes="
- + aadAes + " aadCha=" + aadCha + " chNonce=" + chNonce);
+ + aadAes + " aadCha=" + aadCha);
// Prepare keyring with RSA pair
Path ring = tmp.resolve("ring-rsa.txt");
@@ -237,13 +236,13 @@ public class GuardTest {
Path dec = tmp.resolve("rsa-pt-ch.bin.dec");
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(),
- "--to-alias", rsa.pub, "--alg", "chacha-aead", "--aad-hex", aadCha, "--nonce-hex", chNonce };
+ "--to-alias", rsa.pub, "--alg", "chacha-aead", "--aad-hex", aadCha };
System.out.println("...ChaCha encrypt: " + Arrays.toString(encArgs));
int e = Guard.main(encArgs, new Options());
assertEquals(0, e, "... ChaCha encrypt rc");
String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--keyring", ring.toString(),
- "--priv-alias", rsa.prv, "--alg", "chacha-aead", "--aad-hex", aadCha, "--nonce-hex", chNonce };
+ "--priv-alias", rsa.prv, "--alg", "chacha-aead", "--aad-hex", aadCha };
System.out.println("...ChaCha decrypt: " + Arrays.toString(decArgs));
int d = Guard.main(decArgs, new Options());
assertEquals(0, d, "... ChaCha decrypt rc");
diff --git a/app/src/test/java/zeroecho/KemTest.java b/app/src/test/java/zeroecho/KemTest.java
index 8f97297..208489d 100644
--- a/app/src/test/java/zeroecho/KemTest.java
+++ b/app/src/test/java/zeroecho/KemTest.java
@@ -142,11 +142,10 @@ public class KemTest {
final int gcmTagBits = 128;
final String aadAes = "A1B2C3";
final String aadChaCha = "DEADBEEF";
- final String nonceChaCha = "00112233445566778899AABB";
System.out.println(method);
System.out.println("...params: aesSize=" + aesSize + " chachaSize=" + chachaSize + " gcmTagBits=" + gcmTagBits
- + " aesAAD=" + aadAes + " chachaAAD=" + aadChaCha + " chachaNonce=" + nonceChaCha);
+ + " aesAAD=" + aadAes + " chachaAAD=" + aadChaCha);
// Discover KEM ids via the CLI (ensures we use exactly the ids users will see).
List
* IV and optional AAD are exchanged via a {@link conflux.CtxInterface} set with
- * {@link #setContext(conflux.CtxInterface)}. On ENCRYPT, a fresh IV is
- * generated if absent and stored back; on DECRYPT, IV must be present (from
- * context or header).
+ * {@link #setContext(conflux.CtxInterface)}. Encryption always generates a fresh
+ * IV after atomically claiming the context; a caller-provided IV is never used
+ * for encryption. Decryption requires the IV from the context or encoded header.
*
@@ -86,6 +87,7 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
private final boolean encrypt;
private final AesSpec spec;
private final SecureRandom rnd;
+ private final AtomicReference
* Streaming contexts exchange ephemeral parameters through a Conflux session
- * context using namespaced keys. For ChaCha20 and ChaCha20-Poly1305, a 12-byte
- * nonce is required for each operation. On encryption, if the session context
- * does not provide a nonce, the context generates a fresh value and stores it
- * back into the session; on decryption the nonce must already be present and
- * have the correct length. ChaCha20 also uses an initial counter sourced from
- * {@link ChaChaSpec} and optionally overridden by the session context. When a
- * header codec is configured and a session context is set, encryption prepends
- * a minimal header and decryption reads it first to hydrate the session before
- * initializing the cipher.
+ * context using namespaced keys. Each ChaCha20 and ChaCha20-Poly1305 encryption
+ * context is single-use: after an atomic claim it generates a fresh 12-byte
+ * nonce and stores it back into the session. On decryption the nonce must
+ * already be present and have the correct length. ChaCha20 also uses an initial
+ * counter sourced from {@link ChaChaSpec} and optionally overridden by the
+ * session context. When a header codec is configured and a session context is
+ * set, encryption prepends a minimal header and decryption reads it first to
+ * hydrate the session before initializing the cipher.
* Abstract streaming cipher context for ChaCha algorithms
@@ -109,6 +112,7 @@ abstract class AbstractChaChaCipherContext implements
protected final S spec;
/** Secure random source for nonce generation. */
protected final SecureRandom rnd;
+ private final AtomicReference implements
this.key = key;
this.encrypt = encrypt;
this.spec = spec;
- this.rnd = (rnd != null ? rnd : new SecureRandom());
+ this.rnd = (rnd != null ? rnd : RandomSupport.getRandom());
}
/** {@inheritDoc} */
@@ -188,6 +192,9 @@ abstract class AbstractChaChaCipherContext implements
*/
@Override
public InputStream attach(InputStream upstream) throws IOException {
+ java.util.Objects.requireNonNull(upstream, "upstream must not be null");
+ claimEncryption();
+ boolean attached = false;
try {
final SymmetricHeaderCodec header = spec.header();
final boolean hasCtxHeader = ctx != null && header != null;
@@ -198,7 +205,7 @@ abstract class AbstractChaChaCipherContext implements
}
final Cipher cipher = Cipher.getInstance(jceName());
- final byte[] nonce = ensureNonce(); // generate or require from ctx
+ final byte[] nonce = ensureNonce();
initCipher(cipher, nonce);
InputStream out = // new Stream(in, cipher, jceName()); // same stream pattern as AES
@@ -210,9 +217,14 @@ abstract class AbstractChaChaCipherContext implements
header.writeHeader(hdr, algorithm, ctx);
out = new SequenceInputStream(new ByteArrayInputStream(hdr.toByteArray()), out);
}
- return out;
+ attached = true;
+ return encrypt ? new LifecycleInputStream(out) : out;
} catch (GeneralSecurityException e) {
throw new ProviderFailureException(jceName() + " attach/init failed", e);
+ } finally {
+ if (!attached) {
+ failEncryption();
+ }
}
}
@@ -231,8 +243,8 @@ abstract class AbstractChaChaCipherContext implements
* Ensures a nonce is available in the context.
*
*
- *
*
@@ -241,22 +253,101 @@ abstract class AbstractChaChaCipherContext implements
*/
private byte[] ensureNonce() throws IOException {
final String id = algorithm.id();
- byte[] nonce = (ctx == null) ? null : ctx.get(ConfluxKeys.iv(id));
+ byte[] nonce;
if (encrypt) {
- if (nonce == null) {
- nonce = new byte[NONCE_LEN];
- rnd.nextBytes(nonce);
- if (ctx != null) { // NOPMD
- ctx.put(ConfluxKeys.iv(id), nonce);
- }
- } else if (nonce.length != NONCE_LEN) {
- throw new IOException("Nonce length mismatch: expected 12 bytes, got " + nonce.length);
+ nonce = new byte[NONCE_LEN];
+ rnd.nextBytes(nonce);
+ if (ctx != null) {
+ ctx.put(ConfluxKeys.iv(id), nonce.clone());
}
} else {
+ nonce = (ctx == null) ? null : ctx.get(ConfluxKeys.iv(id));
if (nonce == null || nonce.length != NONCE_LEN) {
throw new IOException("Nonce missing/invalid for " + jceName() + " decryption");
}
}
return nonce;
}
+
+ private void claimEncryption() throws IOException {
+ if (encrypt && !operationState.compareAndSet(OperationState.NEW, OperationState.ENCRYPTING)) {
+ throw new IOException(jceName() + " encryption context is single-use");
+ }
+ }
+
+ private void failEncryption() {
+ if (encrypt) {
+ operationState.set(OperationState.FAILED);
+ }
+ }
+
+ /** Single-use encryption lifecycle states. */
+ private enum OperationState {
+ NEW,
+ ENCRYPTING,
+ COMPLETED,
+ FAILED
+ }
+
+ /** Marks the owning encryption context terminal as its stream is consumed. */
+ private final class LifecycleInputStream extends FilterInputStream {
+ private boolean endOfInput;
+
+ private LifecycleInputStream(InputStream delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public int read() throws IOException {
+ boolean successful = false;
+ try {
+ int value = super.read();
+ successful = true;
+ completeAtEnd(value < 0);
+ return value;
+ } finally {
+ if (!successful) {
+ failEncryption();
+ }
+ }
+ }
+
+ @Override
+ public int read(byte[] bytes, int offset, int length) throws IOException {
+ boolean successful = false;
+ try {
+ int count = super.read(bytes, offset, length);
+ successful = true;
+ completeAtEnd(count < 0);
+ return count;
+ } finally {
+ if (!successful) {
+ failEncryption();
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ boolean successful = false;
+ try {
+ super.close();
+ successful = true;
+ if (!endOfInput) {
+ failEncryption();
+ }
+ } finally {
+ if (!successful) {
+ failEncryption();
+ }
+ }
+ }
+
+ private void completeAtEnd(boolean ended) {
+ if (ended) {
+ endOfInput = true;
+ operationState.compareAndSet(OperationState.ENCRYPTING, OperationState.COMPLETED);
+ }
+ }
+ }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java
index ba07442..ba1259c 100644
--- a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java
@@ -34,6 +34,7 @@
package zeroecho.core.alg.chacha;
import zeroecho.core.SymmetricHeaderCodec;
+import zeroecho.sdk.util.RandomSupport;
/**
* ChaCha20-Poly1305 (AEAD) algorithm
@@ -101,12 +102,12 @@ public final class ChaCha20Poly1305Algorithm extends AbstractChaChaAlgorithm {
super("CHACHA20-POLY1305", "ChaCha20-Poly1305 (AEAD)");
capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.ENCRYPT,
zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, ChaCha20Poly1305Spec.class,
- (k, s) -> new ChaCha20Poly1305CipherContext(this, k, true, s, new java.security.SecureRandom()),
+ (k, s) -> new ChaCha20Poly1305CipherContext(this, k, true, s, RandomSupport.getRandom()),
() -> ChaCha20Poly1305Spec.builder().header(null).build());
capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.DECRYPT,
zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, ChaCha20Poly1305Spec.class,
- (k, s) -> new ChaCha20Poly1305CipherContext(this, k, false, s, new java.security.SecureRandom()),
+ (k, s) -> new ChaCha20Poly1305CipherContext(this, k, false, s, RandomSupport.getRandom()),
() -> ChaCha20Poly1305Spec.builder().header(null).build());
// VoidSpec defaults like AES-GCM
@@ -114,14 +115,14 @@ public final class ChaCha20Poly1305Algorithm extends AbstractChaChaAlgorithm {
zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class,
zeroecho.core.spec.VoidSpec.class,
(k, v) -> new ChaCha20Poly1305CipherContext(this, k, true,
- ChaCha20Poly1305Spec.builder().header(null).build(), new java.security.SecureRandom()),
+ ChaCha20Poly1305Spec.builder().header(null).build(), RandomSupport.getRandom()),
() -> zeroecho.core.spec.VoidSpec.INSTANCE);
capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.DECRYPT,
zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class,
zeroecho.core.spec.VoidSpec.class,
(k, v) -> new ChaCha20Poly1305CipherContext(this, k, false,
- ChaCha20Poly1305Spec.builder().header(null).build(), new java.security.SecureRandom()),
+ ChaCha20Poly1305Spec.builder().header(null).build(), RandomSupport.getRandom()),
() -> zeroecho.core.spec.VoidSpec.INSTANCE);
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305CipherContext.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305CipherContext.java
index 27a3f6b..82ea2e4 100644
--- a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305CipherContext.java
+++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305CipherContext.java
@@ -67,10 +67,10 @@ import zeroecho.core.CryptoAlgorithm;
* ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build();
*
* // Encrypt
- * EncryptionContext enc = new ChaCha20Poly1305CipherContext(alg, key, true, spec, new SecureRandom());
+ * EncryptionContext enc = new ChaCha20Poly1305CipherContext(alg, key, true, spec, null);
*
* // Decrypt
- * EncryptionContext dec = new ChaCha20Poly1305CipherContext(alg, key, false, spec, new SecureRandom());
+ * EncryptionContext dec = new ChaCha20Poly1305CipherContext(alg, key, false, spec, null);
* }
*
* @since 1.0
diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaAlgorithm.java
index 6ccad99..6c6c84b 100644
--- a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaAlgorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaAlgorithm.java
@@ -33,6 +33,7 @@
******************************************************************************/
package zeroecho.core.alg.chacha;
+import zeroecho.sdk.util.RandomSupport;
/**
* ChaCha20 (stream) algorithm
*
@@ -82,12 +83,12 @@ public final class ChaChaAlgorithm extends AbstractChaChaAlgorithm {
// ENCRYPT
capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.ENCRYPT,
zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, ChaChaSpec.class,
- (k, s) -> new ChaChaCipherContext(this, k, true, s, new java.security.SecureRandom()),
+ (k, s) -> new ChaChaCipherContext(this, k, true, s, RandomSupport.getRandom()),
() -> ChaChaSpec.builder().initialCounter(1).header(null).build());
// DECRYPT
capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.DECRYPT,
zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, ChaChaSpec.class,
- (k, s) -> new ChaChaCipherContext(this, k, false, s, new java.security.SecureRandom()),
+ (k, s) -> new ChaChaCipherContext(this, k, false, s, RandomSupport.getRandom()),
() -> ChaChaSpec.builder().initialCounter(1).header(null).build());
// VoidSpec defaults (mirrors AES)
@@ -95,14 +96,14 @@ public final class ChaChaAlgorithm extends AbstractChaChaAlgorithm {
zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class,
zeroecho.core.spec.VoidSpec.class,
(k, v) -> new ChaChaCipherContext(this, k, true,
- ChaChaSpec.builder().initialCounter(1).header(null).build(), new java.security.SecureRandom()),
+ ChaChaSpec.builder().initialCounter(1).header(null).build(), RandomSupport.getRandom()),
() -> zeroecho.core.spec.VoidSpec.INSTANCE);
capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.DECRYPT,
zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class,
zeroecho.core.spec.VoidSpec.class,
(k, v) -> new ChaChaCipherContext(this, k, false,
- ChaChaSpec.builder().initialCounter(1).header(null).build(), new java.security.SecureRandom()),
+ ChaChaSpec.builder().initialCounter(1).header(null).build(), RandomSupport.getRandom()),
() -> zeroecho.core.spec.VoidSpec.INSTANCE);
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java b/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
index 680289b..0220272 100644
--- a/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
@@ -78,23 +78,22 @@
* Runtime parameters and context exchange
* Safety and validation
*
- *
*
* @param encrypt {@code true} for encryption; {@code false} for decryption
@@ -340,6 +339,9 @@ public final class AesDataContentBuilder implements DataContentBuilderRuntime parameters
Optional IV and AAD can be provided via
- * {@link #withIv(byte[])} and {@link #withAad(byte[])}. If a Conflux context is
- * present (set via {@link #context(conflux.CtxInterface)} or implied by a
- * header), a fresh IV is generated on encrypt when absent and stored back into
- * the context. For GCM, AAD defaults to empty.
+ * Runtime parameters
Optional AAD can be provided via
+ * {@link #withAad(byte[])}. Encryption always generates its IV internally.
+ * Headerless decryption may receive an out-of-band IV through
+ * {@link #withDecryptionIv(byte[])}; that configuration is rejected when
+ * building an encrypting pipeline. For GCM, AAD defaults to empty.
*
* Thread‑safety
The builder is not thread‑safe. Built
* {@code DataContent} instances are independent.
@@ -114,7 +114,7 @@ public final class AesDataContentBuilder implements DataContentBuilder{@code
* // 1) ChaCha20 stream encryption with generated key and header
@@ -106,10 +106,10 @@ import zeroecho.sdk.content.api.PlainContent;
* .withHeader()
* .build(true);
*
- * // 2) ChaCha20-Poly1305 decryption with supplied key, nonce, and AAD
+ * // 2) Headerless ChaCha20-Poly1305 decryption with supplied key, nonce, and AAD
* DataContent dec = ChaChaDataContentBuilder.builder()
* .withKey(secretKey)
- * .withNonce(nonce12) // 12 bytes
+ * .withDecryptionNonce(nonce12) // 12 bytes
* .withAad("meta".getBytes(StandardCharsets.UTF_8))
* .build(false);
*
@@ -150,7 +150,7 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder
{@code
@@ -435,20 +436,23 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder 0) {
- ctx.put(ConfluxKeys.iv(algId), nonce);
+ if (decryptionNonce != null && decryptionNonce.length > 0) {
+ ctx.put(ConfluxKeys.iv(algId), decryptionNonce.clone());
}
if (v == Variant.AEAD) {
if (aad != null) { // NOPMD
diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java b/lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java
index dfa02a4..8ecd03a 100644
--- a/lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java
+++ b/lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java
@@ -61,8 +61,6 @@ import zeroecho.sdk.hybrid.kex.HybridKexExporter;
*
*
* - {@code label + "/key"} for the secret key
- * - {@code label + "/iv"} for AES IV
- * - {@code label + "/nonce"} for ChaCha nonce
* - {@code label + "/aad"} for AEAD AAD (optional, if derived)
*
*
@@ -190,7 +188,7 @@ public final class HybridDerived {
}
/**
- * Derives an AES key and applies it (and optional IV/AAD) to the provided AES
+ * Derives an AES key and applies it with optional AAD to the provided AES
* builder.
*
*
@@ -200,15 +198,12 @@ public final class HybridDerived {
*
* @param aes AES builder to configure (must not be null)
* @param keyBits AES key size in bits (128/192/256)
- * @param ivLenBytes if > 0, derive IV of this length and inject it via
- * {@code withIv(...)}; if 0, do not set IV (header/ctx may
- * generate it)
* @return the provided builder instance
* @throws NullPointerException if aes is null
* @throws IllegalArgumentException if keyBits is invalid
* @since 1.0
*/
- public AesDataContentBuilder applyToAesGcm(AesDataContentBuilder aes, int keyBits, int ivLenBytes) {
+ public AesDataContentBuilder applyToAesGcm(AesDataContentBuilder aes, int keyBits) {
Objects.requireNonNull(aes, "aes");
validateBase();
@@ -221,11 +216,6 @@ public final class HybridDerived {
Arrays.fill(keyRaw, (byte) 0);
}
- if (ivLenBytes > 0) {
- byte[] iv = exportBytes(label + "/iv", ivLenBytes);
- aes.withIv(iv);
- }
-
byte[] aad = resolveAad();
if (aad != null) {
aes.withAad(aad);
@@ -235,7 +225,7 @@ public final class HybridDerived {
}
/**
- * Derives a ChaCha key and applies it (and optional nonce/AAD) to the provided
+ * Derives a ChaCha key and applies it with optional AAD to the provided
* ChaCha builder.
*
*
@@ -245,15 +235,12 @@ public final class HybridDerived {
*
* @param chacha ChaCha builder to configure (must not be null)
* @param keyBits key size in bits (typically 256)
- * @param nonceLenBytes if > 0, derive nonce of this length and inject it via
- * {@code withNonce(...)}; if 0, do not set nonce
- * (header/ctx may generate it)
* @return the provided builder instance
* @throws NullPointerException if chacha is null
* @throws IllegalArgumentException if keyBits is invalid
* @since 1.0
*/
- public ChaChaDataContentBuilder applyToChaChaAead(ChaChaDataContentBuilder chacha, int keyBits, int nonceLenBytes) {
+ public ChaChaDataContentBuilder applyToChaChaAead(ChaChaDataContentBuilder chacha, int keyBits) {
Objects.requireNonNull(chacha, "chacha");
validateBase();
@@ -266,11 +253,6 @@ public final class HybridDerived {
Arrays.fill(keyRaw, (byte) 0);
}
- if (nonceLenBytes > 0) {
- byte[] nonce = exportBytes(label + "/nonce", nonceLenBytes);
- chacha.withNonce(nonce);
- }
-
byte[] aad = resolveAad();
if (aad != null) {
chacha.withAad(aad);
diff --git a/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java b/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java
index 2f79c8f..2701113 100644
--- a/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java
+++ b/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java
@@ -128,7 +128,6 @@ public class AesGcmCrossCheckTest {
// --- test vectors ---
byte[] msg = rand(SIZE);
- byte[] iv = rand(12); // 12-byte IV for GCM
byte[] aad = "test-aad-123".getBytes();
// --- key (either via your builder or direct JCA; both fine) ---
@@ -139,10 +138,8 @@ public class AesGcmCrossCheckTest {
// kg.init(256);
// SecretKey key = kg.generateKey();
- // --- per-test context; store IV/AAD under the names AesCipherContext expects
- // ---
+ // --- per-test context; encryption generates IV and stores it here ---
CtxInterface session = Ctx.INSTANCE.getContext("aes-gcm-xchk-" + System.nanoTime());
- session.put(ConfluxKeys.iv("AES"), iv);
session.put(ConfluxKeys.aad("AES"), aad);
AesSpec spec = AesSpec.gcm128(null);
@@ -152,6 +149,7 @@ public class AesGcmCrossCheckTest {
((ContextAware) enc).setContext(session);
byte[] ct_stream = readAll(enc.attach(new ByteArrayInputStream(msg)));
enc.close();
+ byte[] iv = session.get(ConfluxKeys.iv("AES"));
// === JCA ENCRYPT (reference) ===
byte[] ct_jca = jcaGcmEncrypt(key, iv, TAG_BITS, aad, msg);
diff --git a/lib/src/test/java/zeroecho/core/alg/aes/AesRandomSupportTest.java b/lib/src/test/java/zeroecho/core/alg/aes/AesRandomSupportTest.java
index 3d55c6f..0c5e34d 100644
--- a/lib/src/test/java/zeroecho/core/alg/aes/AesRandomSupportTest.java
+++ b/lib/src/test/java/zeroecho/core/alg/aes/AesRandomSupportTest.java
@@ -6,18 +6,28 @@ 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.assertFalse;
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 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.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.security.SecureRandom;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
+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 javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@@ -31,6 +41,7 @@ import zeroecho.core.KeyUsage;
import zeroecho.core.context.EncryptionContext;
import zeroecho.core.spec.VoidSpec;
import zeroecho.core.spi.ContextAware;
+import zeroecho.sdk.builders.alg.AesDataContentBuilder;
import zeroecho.sdk.util.RandomSupport;
class AesRandomSupportTest {
@@ -89,7 +100,113 @@ class AesRandomSupportTest {
assertArrayEquals(repeated((byte) 1, 12), firstIv);
assertArrayEquals(repeated((byte) 2, 12), secondIv);
assertNotSame(firstIv, secondIv);
- System.out.println("...randomCalls=" + random.calls);
+ System.out.println("...randomCalls=" + random.calls());
+ System.out.println("ok");
+ }
+
+ @Test
+ void encryptionIgnoresPreseededIvAndContextIsSingleUse() throws Exception {
+ System.out.print("AesRandomSupport/encryptionIgnoresPreseededIvAndContextIsSingleUse...");
+ CountingSecureRandom random = new CountingSecureRandom();
+ CtxInterface session = Ctx.INSTANCE.getContext("aes-preseeded");
+ session.put(ConfluxKeys.iv("AES"), repeated((byte) 9, 12));
+ AesCipherContext context = new AesCipherContext(new AesAlgorithm(), KEY, true, AesSpec.gcm128(null), random);
+ ((ContextAware) context).setContext(session);
+
+ try (InputStream stream = context.attach(new ByteArrayInputStream(new byte[0]))) {
+ stream.readAllBytes();
+ }
+ IOException failure = assertThrows(IOException.class,
+ () -> context.attach(new ByteArrayInputStream(new byte[0])));
+
+ assertArrayEquals(repeated((byte) 1, 12), session.get(ConfluxKeys.iv("AES")));
+ assertEquals(1, random.calls());
+ assertEquals("AES encryption context is single-use", failure.getMessage());
+ System.out.println("...randomCalls=" + random.calls());
+ System.out.println("ok");
+ }
+
+ @Test
+ void streamFailureConsumesEncryptionContext() throws Exception {
+ System.out.print("AesRandomSupport/streamFailureConsumesEncryptionContext...");
+ CountingSecureRandom random = new CountingSecureRandom();
+ AesCipherContext context = new AesCipherContext(new AesAlgorithm(), KEY, true, AesSpec.gcm128(null), random);
+ ((ContextAware) context).setContext(Ctx.INSTANCE.getContext("aes-stream-failure"));
+
+ InputStream failing = new InputStream() {
+ @Override
+ public int read() throws IOException {
+ throw new IOException("controlled input failure");
+ }
+ };
+ try (InputStream stream = context.attach(failing)) {
+ assertThrows(IOException.class, stream::readAllBytes);
+ }
+ IOException reuse = assertThrows(IOException.class,
+ () -> context.attach(new ByteArrayInputStream(new byte[0])));
+
+ assertEquals("AES encryption context is single-use", reuse.getMessage());
+ assertEquals(1, random.calls());
+ System.out.println("...terminalAfterFailure=true");
+ System.out.println("ok");
+ }
+
+ @Test
+ void concurrentEncryptionClaimHasExactlyOneWinner() throws Exception {
+ System.out.print("AesRandomSupport/concurrentEncryptionClaimHasExactlyOneWinner...");
+ CountingSecureRandom random = new CountingSecureRandom();
+ AesCipherContext context = new AesCipherContext(new AesAlgorithm(), KEY, true, AesSpec.gcm128(null), random);
+ ((ContextAware) context).setContext(Ctx.INSTANCE.getContext("aes-concurrent-claim"));
+ CountDownLatch ready = new CountDownLatch(2);
+ CountDownLatch start = new CountDownLatch(1);
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ try {
+ Callable attempt = () -> {
+ ready.countDown();
+ start.await();
+ try (InputStream stream = context.attach(new ByteArrayInputStream(new byte[0]))) {
+ stream.readAllBytes();
+ return true;
+ } catch (IOException expected) {
+ return false;
+ }
+ };
+ Future first = executor.submit(attempt);
+ Future second = executor.submit(attempt);
+ ready.await();
+ start.countDown();
+
+ assertTrue(first.get() ^ second.get());
+ assertEquals(1, random.calls());
+
+ CountingSecureRandom independentRandom = new CountingSecureRandom();
+ AesCipherContext left = encryptingContext(independentRandom, "aes-independent-left");
+ AesCipherContext right = encryptingContext(independentRandom, "aes-independent-right");
+ Future leftResult = executor.submit(() -> encryptEmpty(left));
+ Future rightResult = executor.submit(() -> encryptEmpty(right));
+ leftResult.get();
+ rightResult.get();
+ assertEquals(2, independentRandom.calls());
+ } finally {
+ executor.close();
+ }
+ System.out.println("...winnerCount=1");
+ System.out.println("...independentRandomCalls=2");
+ System.out.println("ok");
+ }
+
+ @Test
+ void publicBuilderExposesOnlyDecryptionIvConfiguration() throws Exception {
+ System.out.print("AesRandomSupport/publicBuilderExposesOnlyDecryptionIvConfiguration...");
+ List methodNames = Arrays.stream(AesDataContentBuilder.class.getMethods()).map(Method::getName).toList();
+
+ assertFalse(methodNames.contains("withIv"));
+ assertTrue(methodNames.contains("withDecryptionIv"));
+ AesDataContentBuilder builder = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession())
+ .withDecryptionIv(repeated((byte) 1, 12));
+ IllegalStateException failure = assertThrows(IllegalStateException.class, () -> builder.build(true));
+ assertEquals("A caller-supplied IV is valid only for AES decryption", failure.getMessage());
+ System.out.println("...encryptionIvSetter=false");
System.out.println("ok");
}
@@ -130,6 +247,18 @@ class AesRandomSupportTest {
}
}
+ private static AesCipherContext encryptingContext(SecureRandom random, String contextName) {
+ AesCipherContext context = new AesCipherContext(new AesAlgorithm(), KEY, true, AesSpec.gcm128(null), random);
+ ((ContextAware) context).setContext(Ctx.INSTANCE.getContext(contextName));
+ return context;
+ }
+
+ private static byte[] encryptEmpty(AesCipherContext context) throws IOException {
+ try (InputStream stream = context.attach(new ByteArrayInputStream(new byte[0]))) {
+ return stream.readAllBytes();
+ }
+ }
+
private static byte[] repeated(byte value, int length) {
byte[] result = new byte[length];
java.util.Arrays.fill(result, value);
@@ -138,12 +267,16 @@ class AesRandomSupportTest {
private static final class CountingSecureRandom extends SecureRandom {
private static final long serialVersionUID = 1L;
- private int calls;
+ private final AtomicInteger calls = new AtomicInteger();
@Override
public void nextBytes(byte[] bytes) {
- calls++;
- java.util.Arrays.fill(bytes, (byte) calls);
+ int current = calls.incrementAndGet();
+ java.util.Arrays.fill(bytes, (byte) current);
+ }
+
+ private int calls() {
+ return calls.get();
}
}
}
diff --git a/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaNonceLifecycleTest.java b/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaNonceLifecycleTest.java
new file mode 100644
index 0000000..afb7e85
--- /dev/null
+++ b/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaNonceLifecycleTest.java
@@ -0,0 +1,221 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.core.alg.chacha;
+
+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.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+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 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.io.Util;
+import zeroecho.core.spi.ContextAware;
+import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder;
+
+class ChaChaNonceLifecycleTest {
+ private static final String ALGORITHM_ID = "CHACHA20-POLY1305";
+ private static final SecretKey KEY = new SecretKeySpec(new byte[32], "ChaCha20");
+ private static final ChaCha20Poly1305Spec HEADER_SPEC = ChaCha20Poly1305Spec.builder()
+ .header(new ChaCha20Poly1305HeaderCodec()).build();
+ private static final ChaCha20Poly1305Spec CONTEXT_SPEC = ChaCha20Poly1305Spec.builder().header(null).build();
+
+ @Test
+ void encryptionIgnoresPreseededNonceAndContextIsSingleUse() throws Exception {
+ System.out.print("ChaChaNonceLifecycle/encryptionIgnoresPreseededNonceAndContextIsSingleUse...");
+ CountingSecureRandom random = new CountingSecureRandom();
+ CtxInterface operation = Ctx.INSTANCE.getContext("chacha-preseeded");
+ operation.put(ConfluxKeys.iv(ALGORITHM_ID), repeated((byte) 9, 12));
+ ChaCha20Poly1305CipherContext context = encryptingContext(CONTEXT_SPEC, random, operation);
+
+ try (InputStream stream = context.attach(new ByteArrayInputStream(new byte[0]))) {
+ stream.readAllBytes();
+ }
+ IOException failure = assertThrows(IOException.class,
+ () -> context.attach(new ByteArrayInputStream(new byte[0])));
+
+ assertArrayEquals(repeated((byte) 1, 12), operation.get(ConfluxKeys.iv(ALGORITHM_ID)));
+ assertEquals(1, random.calls());
+ assertEquals("ChaCha20-Poly1305 encryption context is single-use", failure.getMessage());
+ System.out.println("...randomCalls=" + random.calls());
+ System.out.println("ok");
+ }
+
+ @Test
+ void streamFailureConsumesEncryptionContext() throws Exception {
+ System.out.print("ChaChaNonceLifecycle/streamFailureConsumesEncryptionContext...");
+ CountingSecureRandom random = new CountingSecureRandom();
+ ChaCha20Poly1305CipherContext context = encryptingContext(CONTEXT_SPEC, random,
+ Ctx.INSTANCE.getContext("chacha-stream-failure"));
+ InputStream failing = new InputStream() {
+ @Override
+ public int read() throws IOException {
+ throw new IOException("controlled input failure");
+ }
+ };
+
+ try (InputStream stream = context.attach(failing)) {
+ assertThrows(IOException.class, stream::readAllBytes);
+ }
+ IOException reuse = assertThrows(IOException.class,
+ () -> context.attach(new ByteArrayInputStream(new byte[0])));
+
+ assertEquals("ChaCha20-Poly1305 encryption context is single-use", reuse.getMessage());
+ assertEquals(1, random.calls());
+ System.out.println("...terminalAfterFailure=true");
+ System.out.println("ok");
+ }
+
+ @Test
+ void concurrentClaimHasOneWinnerWhileSeparateContextsProceed() throws Exception {
+ System.out.print("ChaChaNonceLifecycle/concurrentClaimHasOneWinnerWhileSeparateContextsProceed...");
+ CountingSecureRandom sharedRandom = new CountingSecureRandom();
+ ChaCha20Poly1305CipherContext shared = encryptingContext(CONTEXT_SPEC, sharedRandom,
+ Ctx.INSTANCE.getContext("chacha-concurrent-shared"));
+ CountDownLatch ready = new CountDownLatch(2);
+ CountDownLatch start = new CountDownLatch(1);
+ ExecutorService executor = Executors.newFixedThreadPool(4);
+ try {
+ Callable attempt = () -> {
+ ready.countDown();
+ start.await();
+ try (InputStream stream = shared.attach(new ByteArrayInputStream(new byte[0]))) {
+ stream.readAllBytes();
+ return true;
+ } catch (IOException expected) {
+ return false;
+ }
+ };
+ Future first = executor.submit(attempt);
+ Future second = executor.submit(attempt);
+ ready.await();
+ start.countDown();
+ assertTrue(first.get() ^ second.get());
+ assertEquals(1, sharedRandom.calls());
+
+ CountingSecureRandom independentRandom = new CountingSecureRandom();
+ ChaCha20Poly1305CipherContext left = encryptingContext(CONTEXT_SPEC, independentRandom,
+ Ctx.INSTANCE.getContext("chacha-independent-left"));
+ ChaCha20Poly1305CipherContext right = encryptingContext(CONTEXT_SPEC, independentRandom,
+ Ctx.INSTANCE.getContext("chacha-independent-right"));
+ Future leftResult = executor.submit(() -> encryptEmpty(left));
+ Future rightResult = executor.submit(() -> encryptEmpty(right));
+ leftResult.get();
+ rightResult.get();
+ assertEquals(2, independentRandom.calls());
+ } finally {
+ executor.close();
+ }
+ System.out.println("...sharedWinners=1");
+ System.out.println("...independentRandomCalls=2");
+ System.out.println("ok");
+ }
+
+ @Test
+ void encodedNonceDrivesDecryptionAndMalformedNonceFailsClosed() throws Exception {
+ System.out.print("ChaChaNonceLifecycle/encodedNonceDrivesDecryptionAndMalformedNonceFailsClosed...");
+ CountingSecureRandom random = new CountingSecureRandom();
+ CtxInterface encryptionOperation = Ctx.INSTANCE.getContext("chacha-header-encrypt");
+ byte[] plaintext = "nonce lifecycle".getBytes(java.nio.charset.StandardCharsets.UTF_8);
+ byte[] ciphertext;
+ try (InputStream stream = encryptingContext(HEADER_SPEC, random, encryptionOperation)
+ .attach(new ByteArrayInputStream(plaintext))) {
+ ciphertext = stream.readAllBytes();
+ }
+
+ assertArrayEquals(repeated((byte) 1, 12), Util.read(new ByteArrayInputStream(ciphertext), 12));
+ assertArrayEquals(plaintext, decryptHeader(ciphertext));
+
+ byte[] modified = ciphertext.clone();
+ modified[1] ^= 1;
+ assertThrows(IOException.class, () -> decryptHeader(modified));
+ assertThrows(IOException.class, () -> decryptHeader(Arrays.copyOf(ciphertext, 11)));
+ assertEquals(1, random.calls());
+ System.out.println("...roundTrip=true");
+ System.out.println("...malformedRejected=true");
+ System.out.println("ok");
+ }
+
+ @Test
+ void publicBuilderExposesOnlyDecryptionNonceConfiguration() {
+ System.out.print("ChaChaNonceLifecycle/publicBuilderExposesOnlyDecryptionNonceConfiguration...");
+ List methodNames = Arrays.stream(ChaChaDataContentBuilder.class.getMethods()).map(Method::getName)
+ .toList();
+
+ assertFalse(methodNames.contains("withNonce"));
+ assertTrue(methodNames.contains("withDecryptionNonce"));
+ ChaChaDataContentBuilder builder = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession())
+ .withDecryptionNonce(repeated((byte) 1, 12));
+ IllegalStateException failure = assertThrows(IllegalStateException.class, () -> builder.build(true));
+ assertEquals("A caller-supplied nonce is valid only for ChaCha decryption", failure.getMessage());
+ System.out.println("...encryptionNonceSetter=false");
+ System.out.println("ok");
+ }
+
+ private static ChaCha20Poly1305CipherContext encryptingContext(ChaCha20Poly1305Spec spec, SecureRandom random,
+ CtxInterface operation) {
+ ChaCha20Poly1305CipherContext context = new ChaCha20Poly1305CipherContext(
+ new ChaCha20Poly1305Algorithm(), KEY, true, spec, random);
+ ((ContextAware) context).setContext(operation);
+ return context;
+ }
+
+ private static byte[] encryptEmpty(ChaCha20Poly1305CipherContext context) throws IOException {
+ try (InputStream stream = context.attach(new ByteArrayInputStream(new byte[0]))) {
+ return stream.readAllBytes();
+ }
+ }
+
+ private static byte[] decryptHeader(byte[] ciphertext) throws Exception {
+ ChaCha20Poly1305CipherContext context = new ChaCha20Poly1305CipherContext(
+ new ChaCha20Poly1305Algorithm(), KEY, false, HEADER_SPEC, null);
+ ((ContextAware) context).setContext(Ctx.INSTANCE.getContext("chacha-header-decrypt-" + System.nanoTime()));
+ try (InputStream stream = context.attach(new ByteArrayInputStream(ciphertext))) {
+ return stream.readAllBytes();
+ }
+ }
+
+ private static byte[] repeated(byte value, int length) {
+ byte[] result = new byte[length];
+ Arrays.fill(result, value);
+ return result;
+ }
+
+ private static final class CountingSecureRandom extends SecureRandom {
+ private static final long serialVersionUID = 1L;
+ private final AtomicInteger calls = new AtomicInteger();
+
+ @Override
+ public void nextBytes(byte[] bytes) {
+ int current = calls.incrementAndGet();
+ Arrays.fill(bytes, (byte) current);
+ }
+
+ private int calls() {
+ return calls.get();
+ }
+ }
+}
diff --git a/lib/src/test/java/zeroecho/sdk/hybrid/derived/HybridDerivedTest.java b/lib/src/test/java/zeroecho/sdk/hybrid/derived/HybridDerivedTest.java
index 93539b4..a0e08b9 100644
--- a/lib/src/test/java/zeroecho/sdk/hybrid/derived/HybridDerivedTest.java
+++ b/lib/src/test/java/zeroecho/sdk/hybrid/derived/HybridDerivedTest.java
@@ -79,7 +79,7 @@ public class HybridDerivedTest {
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);
+ .aad(aad).applyToAesGcm(encAes, 256);
System.out.println("...returnedEncSame=" + (returnedEnc == encAes));
assertSame(encAes, returnedEnc);
@@ -90,8 +90,8 @@ public class HybridDerivedTest {
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);
+ HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript).aad(aad)
+ .applyToAesGcm(decAes, 256);
byte[] out = runDecrypt(decAes, ciphertext);
System.out.println("...outPrefix=" + shortHex(out, 32));
@@ -111,8 +111,8 @@ public class HybridDerivedTest {
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);
+ HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript).aad(aad)
+ .applyToAesGcm(encAes, 256);
byte[] ciphertext = runEncrypt(encAes, msg);
System.out.println("...ciphertextLen=" + ciphertext.length);
@@ -121,7 +121,7 @@ public class HybridDerivedTest {
// ...label mismatch -> wrong key/iv/aad -> decryption must fail
HybridDerived.from(exporter).label("app/enc/aes_WRONG").transcript(transcript).aad(aad)
- .applyToAesGcm(decAesWrong, 256, 12);
+ .applyToAesGcm(decAesWrong, 256);
assertThrows(Exception.class, () -> runDecrypt(decAesWrong, ciphertext));
@@ -140,7 +140,7 @@ public class HybridDerivedTest {
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);
+ .transcript(transcript).aad(aad).applyToChaChaAead(encChaCha, 256);
System.out.println("...returnedEncSame=" + (returnedEnc == encChaCha));
assertSame(encChaCha, returnedEnc);
@@ -152,7 +152,7 @@ public class HybridDerivedTest {
ChaChaDataContentBuilder decChaCha = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader();
HybridDerived.from(exporter).label("app/enc/chacha").transcript(transcript).aad(aad)
- .applyToChaChaAead(decChaCha, 256, 12);
+ .applyToChaChaAead(decChaCha, 256);
byte[] out = runDecrypt(decChaCha, ciphertext);
System.out.println("...outPrefix=" + shortHex(out, 32));
diff --git a/samples/src/test/java/demo/HybridDerivedAesDemoTest.java b/samples/src/test/java/demo/HybridDerivedAesDemoTest.java
index c9e2598..930e5df 100644
--- a/samples/src/test/java/demo/HybridDerivedAesDemoTest.java
+++ b/samples/src/test/java/demo/HybridDerivedAesDemoTest.java
@@ -204,12 +204,12 @@ class HybridDerivedAesDemoTest {
.transcript(transcript.toByteArray())
// ...Inject explicit AAD.
.aad(aad)
- // ...Apply derived key(256b) and IV(12B) to AES-GCM with header.
+ // ...Apply the derived key to AES-GCM; encryption creates the IV.
.applyToAesGcm(AesDataContentBuilder.builder(session)
// ...Store IV in header for decrypt side.
.withHeader()
// ...Use AES-GCM with 128-bit authentication tag.
- .modeGcm(128), 256, 12))
+ .modeGcm(128), 256))
// ...Finalize pipeline.
.build();
@@ -223,7 +223,7 @@ class HybridDerivedAesDemoTest {
DataContent dec = DataContentChainBuilder.decrypt()
// ...Input: ciphertext bytes.
.add(PlainBytesBuilder.builder().bytes(ciphertext))
- // ...AEAD: apply the same label/transcript/AAD to get identical key/IV.
+ // ...AEAD: apply the same label/transcript/AAD to get the identical key.
.add(HybridDerived.from(exporter)
// ...Same purpose label as encryption.
.label("app/enc/aes-gcm")
@@ -231,12 +231,12 @@ class HybridDerivedAesDemoTest {
.transcript(transcript.toByteArray())
// ...Same explicit AAD as encryption.
.aad(aad)
- // ...Apply derived key and IV to AES-GCM with header.
+ // ...Apply the derived key; decryption reads the IV from the header.
.applyToAesGcm(AesDataContentBuilder.builder(session)
// ...Parse IV from header.
.withHeader()
// ...Use AES-GCM with 128-bit authentication tag.
- .modeGcm(128), 256, 12))
+ .modeGcm(128), 256))
// ...Finalize pipeline.
.build();
@@ -365,7 +365,7 @@ class HybridDerivedAesDemoTest {
// ...Use AES-GCM with 128-bit authentication tag.
aesEnc.modeGcm(128);
- // ...Inject derived key/IV/AAD into AES builder.
+ // ...Inject the derived key and AAD into the AES builder.
HybridDerived.from(exporter)
// ...Purpose separation label for AEAD.
.label("app/enc/aes-gcm")
@@ -373,8 +373,8 @@ class HybridDerivedAesDemoTest {
.transcript(transcript.toByteArray())
// ...Inject explicit AAD.
.aad(aad)
- // ...Apply derived key(256b) and IV(12B).
- .applyToAesGcm(aesEnc, 256, 12);
+ // ...Apply the derived 256-bit key; encryption creates the IV.
+ .applyToAesGcm(aesEnc, 256);
// ...Build encryption pipeline.
DataContent enc = DataContentChainBuilder.encrypt()
@@ -398,7 +398,7 @@ class HybridDerivedAesDemoTest {
// ...Use AES-GCM with 128-bit authentication tag.
aesDec.modeGcm(128);
- // ...Inject the same derived key/IV/AAD into decryption builder.
+ // ...Inject the same derived key and AAD into the decryption builder.
HybridDerived.from(exporter)
// ...Same purpose label.
.label("app/enc/aes-gcm")
@@ -406,8 +406,8 @@ class HybridDerivedAesDemoTest {
.transcript(transcript.toByteArray())
// ...Same explicit AAD.
.aad(aad)
- // ...Apply the same derived key and IV.
- .applyToAesGcm(aesDec, 256, 12);
+ // ...Apply the same derived key; the IV is read from the header.
+ .applyToAesGcm(aesDec, 256);
// ...Build decryption pipeline.
DataContent dec = DataContentChainBuilder.decrypt()
@@ -512,12 +512,12 @@ class HybridDerivedAesDemoTest {
.transcript(transcript.toByteArray())
// ...Inject explicit AAD.
.aad(aad)
- // ...Apply derived key(256b) and IV(12B) to AES-GCM with header.
+ // ...Apply the derived key to AES-GCM; encryption creates the IV.
.applyToAesGcm(AesDataContentBuilder.builder(session)
// ...Store IV in header for decrypt side.
.withHeader()
// ...Use AES-GCM with 128-bit authentication tag.
- .modeGcm(128), 256, 12))
+ .modeGcm(128), 256))
// ...Finalize pipeline.
.build();
@@ -572,7 +572,7 @@ class HybridDerivedAesDemoTest {
DataContent dec = DataContentChainBuilder.decrypt()
// ...Input: ciphertext bytes.
.add(PlainBytesBuilder.builder().bytes(ciphertext))
- // ...AEAD: apply the same label/transcript/AAD to get identical key/IV.
+ // ...AEAD: apply the same label/transcript/AAD to get the identical key.
.add(HybridDerived.from(exporterDec)
// ...Same purpose label as encryption.
.label("app/local/aes-gcm")
@@ -580,12 +580,12 @@ class HybridDerivedAesDemoTest {
.transcript(transcript.toByteArray())
// ...Same explicit AAD.
.aad(aad)
- // ...Apply derived key and IV to AES-GCM with header.
+ // ...Apply the derived key; decryption reads the IV from the header.
.applyToAesGcm(AesDataContentBuilder.builder(session)
// ...Parse IV from header.
.withHeader()
// ...Use AES-GCM with 128-bit authentication tag.
- .modeGcm(128), 256, 12))
+ .modeGcm(128), 256))
// ...Finalize pipeline.
.build();