security(lib): enforce single-use encryption contexts
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user