security(lib): enforce single-use encryption contexts
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<String> kemIds = listKemsViaCli();
|
||||
@@ -209,14 +208,14 @@ public class KemTest {
|
||||
System.out.println("...[" + kemId + "] ChaCha encrypt");
|
||||
int e = Kem.main(new String[] { "--encrypt", plain.toString(), "--output", enc.toString(),
|
||||
"--keyring", ring.toString(), "--pub", aliases.pub, "--kem", kemId, "--chacha",
|
||||
"--chacha-nonce", nonceChaCha, "--aad", aadChaCha, "--header" }, new Options());
|
||||
"--aad", aadChaCha, "--header" }, new Options());
|
||||
if (e != 0) {
|
||||
throw new IllegalStateException("ChaCha encrypt rc=" + e);
|
||||
}
|
||||
System.out.println("...[" + kemId + "] ChaCha decrypt");
|
||||
int d = Kem.main(new String[] { "--decrypt", enc.toString(), "--output", dec.toString(),
|
||||
"--keyring", ring.toString(), "--priv", aliases.prv, "--kem", kemId, "--chacha",
|
||||
"--chacha-nonce", nonceChaCha, "--aad", aadChaCha, "--header" }, new Options());
|
||||
"--aad", aadChaCha, "--header" }, new Options());
|
||||
if (d != 0) {
|
||||
throw new IllegalStateException("ChaCha decrypt rc=" + d);
|
||||
}
|
||||
|
||||
@@ -34,10 +34,12 @@
|
||||
package zeroecho.core.alg.aes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -54,7 +56,6 @@ import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.err.ProviderFailureException;
|
||||
import zeroecho.core.io.CipherTransformInputStreamBuilder;
|
||||
import zeroecho.core.spi.ContextAware;
|
||||
import zeroecho.core.util.Strings;
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
|
||||
/**
|
||||
@@ -62,9 +63,9 @@ import zeroecho.sdk.util.RandomSupport;
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
@@ -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<OperationState> operationState = new AtomicReference<>(OperationState.NEW);
|
||||
|
||||
private volatile CtxInterface ctx; // NOPMD
|
||||
|
||||
@@ -172,6 +174,8 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
|
||||
@Override
|
||||
public InputStream attach(InputStream upstream) throws IOException {
|
||||
Objects.requireNonNull(upstream, "upstream must not be null");
|
||||
claimEncryption();
|
||||
boolean attached = false;
|
||||
try {
|
||||
// If both spec.header() and ctx are present, let this context read/write the
|
||||
// header.
|
||||
@@ -186,8 +190,7 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
|
||||
}
|
||||
|
||||
Cipher cipher = Cipher.getInstance(jcaTransform(spec));
|
||||
initCipher(cipher); // consumes IV/AAD from ctx if present; generates IV on ENCRYPT and may store it
|
||||
// back; sets tagBits for GCM
|
||||
initCipher(cipher);
|
||||
|
||||
InputStream out = // new Stream(in, cipher, spec);
|
||||
CipherTransformInputStreamBuilder.builder().withCipher(cipher).withUpstream(in)
|
||||
@@ -200,9 +203,14 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
|
||||
header.writeHeader(hdr, algorithm, ctx);
|
||||
out = new java.io.SequenceInputStream(new java.io.ByteArrayInputStream(hdr.toByteArray()), out);
|
||||
}
|
||||
return out;
|
||||
attached = true;
|
||||
return encrypt ? new LifecycleInputStream(out) : out;
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new ProviderFailureException("AES attach/init failed", e);
|
||||
} finally {
|
||||
if (!attached) {
|
||||
failEncryption();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,21 +229,18 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
|
||||
};
|
||||
}
|
||||
|
||||
private void initCipher(Cipher cipher) throws GeneralSecurityException, IOException { // NOPMD
|
||||
private void initCipher(Cipher cipher) throws GeneralSecurityException, IOException {
|
||||
final String id = algorithm.id();
|
||||
byte[] iv = getCtxBytes(ConfluxKeys.iv(id));
|
||||
byte[] iv;
|
||||
|
||||
final int ivLen = (spec.mode() == AesSpec.Mode.GCM) ? GCM_DEFAULT_IV_BYTES : AES_BLOCK;
|
||||
|
||||
if (encrypt) {
|
||||
if (iv == null) {
|
||||
iv = new byte[ivLen];
|
||||
rnd.nextBytes(iv);
|
||||
putCtxBytes(ConfluxKeys.iv(id), iv);
|
||||
} else if (iv.length != ivLen) {
|
||||
throw new IOException("IV length mismatch: expected " + ivLen + " bytes, got " + iv.length);
|
||||
}
|
||||
iv = new byte[ivLen];
|
||||
rnd.nextBytes(iv);
|
||||
putCtxBytes(ConfluxKeys.iv(id), iv.clone());
|
||||
} else {
|
||||
iv = getCtxBytes(ConfluxKeys.iv(id));
|
||||
if (iv == null) {
|
||||
throw new IOException("IV not found in context for AES " + spec.mode() + " decryption");
|
||||
}
|
||||
@@ -267,10 +272,7 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
|
||||
}
|
||||
cipher.updateAAD(aad);
|
||||
|
||||
if (LOG.isLoggable(Level.FINE)) {
|
||||
LOG.log(Level.FINE, "GCM setup: tagBits={0} iv={1} aad={2}",
|
||||
new Object[] { tagBits, Strings.toShortHexString(iv), Strings.toShortHexString(aad) });
|
||||
}
|
||||
LOG.log(Level.FINE, "GCM setup complete: tagBits={0}", tagBits);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -294,4 +296,86 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
|
||||
ctx.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void claimEncryption() throws IOException {
|
||||
if (encrypt && !operationState.compareAndSet(OperationState.NEW, OperationState.ENCRYPTING)) {
|
||||
throw new IOException("AES 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@ package zeroecho.core.alg.chacha;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.SequenceInputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
@@ -52,6 +54,7 @@ import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.err.ProviderFailureException;
|
||||
import zeroecho.core.io.CipherTransformInputStreamBuilder;
|
||||
import zeroecho.core.spi.ContextAware;
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
|
||||
/**
|
||||
* <h2>Abstract streaming cipher context for ChaCha algorithms</h2>
|
||||
@@ -109,6 +112,7 @@ abstract class AbstractChaChaCipherContext<S extends ChaChaBaseSpec> implements
|
||||
protected final S spec;
|
||||
/** Secure random source for nonce generation. */
|
||||
protected final SecureRandom rnd;
|
||||
private final AtomicReference<OperationState> operationState = new AtomicReference<>(OperationState.NEW);
|
||||
/** Optional per-operation context for exchanging headers, IVs, etc. */
|
||||
protected CtxInterface ctx; // optional
|
||||
|
||||
@@ -127,7 +131,7 @@ abstract class AbstractChaChaCipherContext<S extends ChaChaBaseSpec> 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<S extends ChaChaBaseSpec> 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<S extends ChaChaBaseSpec> 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<S extends ChaChaBaseSpec> 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<S extends ChaChaBaseSpec> implements
|
||||
* Ensures a nonce is available in the context.
|
||||
*
|
||||
* <ul>
|
||||
* <li>For encryption, generates a new nonce if absent and stores it in
|
||||
* context.</li>
|
||||
* <li>For encryption, always generates a new nonce after the context has
|
||||
* been atomically claimed and stores a copy in the context.</li>
|
||||
* <li>For decryption, validates presence and correct length.</li>
|
||||
* </ul>
|
||||
*
|
||||
@@ -241,22 +253,101 @@ abstract class AbstractChaChaCipherContext<S extends ChaChaBaseSpec> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
package zeroecho.core.alg.chacha;
|
||||
|
||||
import zeroecho.core.SymmetricHeaderCodec;
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
|
||||
/**
|
||||
* <h2>ChaCha20-Poly1305 (AEAD) algorithm</h2>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
* }</pre>
|
||||
*
|
||||
* @since 1.0
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.chacha;
|
||||
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
/**
|
||||
* <h2>ChaCha20 (stream) algorithm</h2>
|
||||
*
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,23 +78,22 @@
|
||||
* <h2>Runtime parameters and context exchange</h2>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Safety and validation</h2>
|
||||
* <ul>
|
||||
* <li><b>Nonce uniqueness:</b> Applications must ensure nonces are unique per
|
||||
* key. The contexts will generate nonces for encryption, but cross-process
|
||||
* uniqueness is the caller's responsibility. Decryption fails if a nonce is
|
||||
* missing or has an unexpected size.</li>
|
||||
* <li><b>Nonce lifecycle:</b> Each encryption context is single-use and
|
||||
* generates its nonce internally after an atomic operation claim. Decryption
|
||||
* consumes the encoded or out-of-band nonce and fails if it is missing or has
|
||||
* an unexpected size.</li>
|
||||
*
|
||||
* <li><b>Counter policy (ChaCha20):</b> The default initial counter is 1. A
|
||||
* context may override the spec value through the session key dedicated to
|
||||
|
||||
@@ -94,11 +94,11 @@ import zeroecho.sdk.content.api.PlainContent;
|
||||
* try (InputStream s = dec.getStream()) { s.transferTo(Files.newOutputStream(ptPath)); }
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Runtime parameters</h2> 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.
|
||||
* <h2>Runtime parameters</h2> 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.
|
||||
*
|
||||
* <h2>Thread‑safety</h2> The builder is not thread‑safe. Built
|
||||
* {@code DataContent} instances are independent.
|
||||
@@ -114,7 +114,7 @@ public final class AesDataContentBuilder implements DataContentBuilder<DataConte
|
||||
|
||||
private final AesSpec.Builder _spec = AesSpec.builder();
|
||||
|
||||
private byte[] iv; // optional
|
||||
private byte[] decryptionIv; // optional
|
||||
private byte[] aad; // optional
|
||||
|
||||
private SymmetricHeaderCodec headerCodec; // optional; carried by AesSpec
|
||||
@@ -264,15 +264,14 @@ public final class AesDataContentBuilder implements DataContentBuilder<DataConte
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies an explicit IV/nonce. If omitted during ENCRYPT and a context is
|
||||
* present, a fresh IV is generated and stored in the context.
|
||||
* Supplies an out-of-band IV for decryption without an encoded header.
|
||||
*
|
||||
* @param iv the IV (12 bytes for GCM, 16 bytes for CBC/CTR)
|
||||
* @return this builder
|
||||
* @throws IllegalArgumentException if the length does not match the mode
|
||||
* @throws NullPointerException if {@code iv} is {@code null}
|
||||
*/
|
||||
public AesDataContentBuilder withIv(byte[] iv) {
|
||||
this.iv = iv;
|
||||
public AesDataContentBuilder withDecryptionIv(byte[] iv) {
|
||||
this.decryptionIv = Objects.requireNonNull(iv, "iv must not be null").clone();
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -330,7 +329,7 @@ public final class AesDataContentBuilder implements DataContentBuilder<DataConte
|
||||
* <li>Resolves the key from {@link #withKey(SecretKey)} or the selected
|
||||
* generate/import spec.</li>
|
||||
* <li>Finalizes {@link AesSpec} (injecting the header if configured).</li>
|
||||
* <li>Propagates IV/AAD into the context if supplied.</li>
|
||||
* <li>Propagates a decryption IV and AAD into the context when supplied.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param encrypt {@code true} for encryption; {@code false} for decryption
|
||||
@@ -340,6 +339,9 @@ public final class AesDataContentBuilder implements DataContentBuilder<DataConte
|
||||
*/
|
||||
@Override
|
||||
public DataContent build(boolean encrypt) {
|
||||
if (encrypt && decryptionIv != null) {
|
||||
throw new IllegalStateException("A caller-supplied IV is valid only for AES decryption");
|
||||
}
|
||||
final SecretKey key = resolveKey();
|
||||
|
||||
// finalize AesSpec from builder + header
|
||||
@@ -358,11 +360,11 @@ public final class AesDataContentBuilder implements DataContentBuilder<DataConte
|
||||
}
|
||||
ctx.put(ConfluxKeys.aad(ALGORITHM_ID), aad);
|
||||
}
|
||||
if (iv != null && iv.length > 0) {
|
||||
if (decryptionIv != null && decryptionIv.length > 0) {
|
||||
if (ctx == null) {
|
||||
ctx = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
|
||||
}
|
||||
ctx.put(ConfluxKeys.iv(ALGORITHM_ID), iv);
|
||||
ctx.put(ConfluxKeys.iv(ALGORITHM_ID), decryptionIv.clone());
|
||||
}
|
||||
|
||||
return encrypt ? new EncryptContent(key, aesSpec, ctx) : new DecryptContent(key, aesSpec, ctx);
|
||||
@@ -526,8 +528,7 @@ public final class AesDataContentBuilder implements DataContentBuilder<DataConte
|
||||
if (!(enc instanceof ContextAware)) {
|
||||
throw new IllegalStateException("AES context is not ContextAware; cannot pass conflux Ctx");
|
||||
}
|
||||
((ContextAware) enc).setContext(ctx); // ctx may be null → header disabled; IV generated only if ctx
|
||||
// present
|
||||
((ContextAware) enc).setContext(ctx);
|
||||
return enc.attach(upstream.getStream());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,9 @@ import zeroecho.sdk.content.api.PlainContent;
|
||||
* is supplied via {@link #withHeaderCodec(SymmetricHeaderCodec)}, a context is
|
||||
* required; if none was provided, a temporary one is created internally. A
|
||||
* default header codec is chosen based on the variant when no custom codec is
|
||||
* set. The nonce is typically 12 bytes; if absent during encryption and a
|
||||
* context is available, an implementation-specific nonce may be generated and
|
||||
* stored in the context.
|
||||
* set. Encryption generates a 12-byte nonce internally after claiming its
|
||||
* single-use context. Headerless decryption may receive a nonce through the
|
||||
* decryption-only configuration method.
|
||||
*
|
||||
* <h2>Usage examples</h2> <pre>{@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<DataCo
|
||||
private ChaChaKeyImportSpec importSpec;
|
||||
|
||||
private CtxInterface ctx;
|
||||
private byte[] nonce;
|
||||
private byte[] decryptionNonce;
|
||||
|
||||
private int initialCounter = 1;
|
||||
private boolean initialCounterSet; // = false;
|
||||
@@ -300,19 +300,20 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder<DataCo
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an optional nonce value.
|
||||
* Sets an out-of-band nonce for headerless decryption.
|
||||
*
|
||||
* <p>
|
||||
* For ChaCha20 and ChaCha20-Poly1305 this is typically 12 bytes. If absent
|
||||
* during encryption and a context is in use, an implementation-specific nonce
|
||||
* may be generated and placed into the context.
|
||||
* For ChaCha20 and ChaCha20-Poly1305 this must be 12 bytes. The value is
|
||||
* rejected when building an encrypting pipeline because encryption always
|
||||
* generates its nonce internally.
|
||||
* </p>
|
||||
*
|
||||
* @param nonce the nonce bytes; may be null
|
||||
* @param nonce the nonce bytes; must not be null
|
||||
* @return {@code this} builder for chaining
|
||||
* @throws NullPointerException if {@code nonce} is {@code null}
|
||||
*/
|
||||
public ChaChaDataContentBuilder withNonce(byte[] nonce) {
|
||||
this.nonce = nonce;
|
||||
public ChaChaDataContentBuilder withDecryptionNonce(byte[] nonce) {
|
||||
this.decryptionNonce = Objects.requireNonNull(nonce, "nonce must not be null").clone();
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -415,8 +416,8 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder<DataCo
|
||||
*
|
||||
* <p>
|
||||
* If a header is requested and no context is provided, a temporary context is
|
||||
* created. When encrypting with a context, if no nonce is supplied, the
|
||||
* implementation may generate one and store it into the context.
|
||||
* created. Encryption generates its nonce internally; a configured decryption
|
||||
* nonce is rejected for encrypting pipelines.
|
||||
* </p>
|
||||
*
|
||||
* <pre>{@code
|
||||
@@ -435,20 +436,23 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder<DataCo
|
||||
*/
|
||||
@Override
|
||||
public DataContent build(boolean encrypt) { // NOPMD
|
||||
if (encrypt && decryptionNonce != null) {
|
||||
throw new IllegalStateException("A caller-supplied nonce is valid only for ChaCha decryption");
|
||||
}
|
||||
final Variant v = inferVariant();
|
||||
final String algId = v == Variant.AEAD ? "CHACHA20-POLY1305" : "CHACHA20";
|
||||
|
||||
final SecretKey key = resolveKey(algId);
|
||||
|
||||
// header → ensure ctx exists
|
||||
if ((headerRequested || headerCodec != null) && ctx == null) {
|
||||
if ((headerRequested || headerCodec != null || decryptionNonce != null) && ctx == null) {
|
||||
ctx = Ctx.INSTANCE
|
||||
.getContext("chacha-" + (v == Variant.AEAD ? "aead" : "stream") + "-" + System.nanoTime());
|
||||
}
|
||||
// fill ctx with runtime params
|
||||
if (ctx != null) {
|
||||
if (nonce != null && nonce.length > 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
|
||||
|
||||
@@ -61,8 +61,6 @@ import zeroecho.sdk.hybrid.kex.HybridKexExporter;
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@code label + "/key"} for the secret key</li>
|
||||
* <li>{@code label + "/iv"} for AES IV</li>
|
||||
* <li>{@code label + "/nonce"} for ChaCha nonce</li>
|
||||
* <li>{@code label + "/aad"} for AEAD AAD (optional, if derived)</li>
|
||||
* </ul>
|
||||
*
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Boolean> 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<Boolean> first = executor.submit(attempt);
|
||||
Future<Boolean> 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<byte[]> leftResult = executor.submit(() -> encryptEmpty(left));
|
||||
Future<byte[]> 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<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Boolean> 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<Boolean> first = executor.submit(attempt);
|
||||
Future<Boolean> 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<byte[]> leftResult = executor.submit(() -> encryptEmpty(left));
|
||||
Future<byte[]> 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<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user