Package zeroecho.sdk.guard
package zeroecho.sdk.guard
Multi-recipient envelope for symmetric payloads with pluggable recipients,
reusable openers, and a compact header.
Overview
This package implements a streaming envelope that protects one content-encryption key (CEK) for multiple recipients and delegates the actual payload protection to a symmetric data-content builder (for example, AES-GCM or AES-CBC). At encrypt time the envelope generates a fresh CEK, creates a table of recipient entries that can recover that CEK, and then streams the symmetric stage header and ciphertext. At decrypt time openers scan the recipient table using a single piece of unlocking material until one opener recovers the CEK, after which the symmetric stage processes the remainder of the stream.Header format
The envelope header is self-delimiting and precedes the symmetric stage bytes. All lengths are encoded as unsigned pack7 integers and all byte sequences are length-prefixed by pack7.| Field | Type | Encoding | Description |
|---|---|---|---|
| recipientCount | int | pack7 | Number of recipient entries that follow. |
| Repeated for i = 1..recipientCount | |||
| entryId | String | pack7 length + UTF-8 bytes | Human-readable identifier that helps route to a matching opener, for example "RSA:OAEP-SHA256" or "KEM:ML-KEM:GCM-WRAP". |
| entryBlob | byte[] | pack7 length + bytes | Opaque per-recipient data sufficient for a matching opener to recover the CEK. Examples include RSA-wrapped CEK, KEM ciphertext plus salts and IVs, or AEAD-wrapped CEK. |
| End repeated section | |||
| symmetricHeader | byte[] | builder-defined | Optional header owned and emitted by the symmetric stage (for example, IV, tag size, or AAD hash), if the chosen symmetric builder is configured to write one. |
| ciphertext | byte stream | builder-defined | Payload bytes produced by the symmetric stage. |
Key ideas
- Separation of concerns: the envelope manages CEK generation and recipient entries; the symmetric builder manages algorithm parameters and payload framing.
- Reusable opener strategies: recipients encode entries; openers attempt every applicable entry and create fresh cryptographic contexts per attempt. Neither carries long-lived secret state.
- Defensive parsing: the builder applies limits to the number of recipients and the size of each entry blob; the symmetric stage applies its own limits to its header and payload.
Design decision: separate unlocking material from openers
This package intentionally keepsUnlockMaterial
separate from RecipientOpener. Openers encode how
to interpret and attempt a specific entry type and are reusable and
stateless; unlocking material centralizes ownership of secrets (private keys
or passwords), which simplifies secret lifecycle management and allows the
envelope to try one piece of material across heterogeneous entries in a
uniform scan. Custom openers receive the borrowed material for each attempt
and must not retain it.
Hardening options
The following options are design targets and not yet implemented in this package:- Label suppression: omit human-readable
entryIdstrings and write empty identifiers, forcing openers to attempt blind parses of entry blobs. - Entry padding or bucketing: normalize entry blob sizes to reduce size-based fingerprinting.
Typical usage
ZeroEchoSession session = new ZeroEchoSession();
EncryptionContext rsaRecipient =
session.createContext("RSA", KeyUsage.ENCRYPT, rsaPub);
KemContext kemRecipient =
session.createContext("ML-KEM", KeyUsage.ENCAPSULATE, kemPub);
// Encrypt for multiple recipients; AES-256/GCM carries its own header.
MultiRecipientContent enc = MultiRecipientDataSourceBuilder.builder(session)
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
.payloadKeyBytes(32)
.addRecipient(rsaRecipient)
.addRecipient(kemRecipient, 32, 16)
.build(true);
enc.setInput(plainSource);
// Decrypt with any one unlocking material; openers are installed by default.
MultiRecipientContent dec = MultiRecipientDataSourceBuilder.builder(session)
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
.payloadKeyBytes(32)
.unlockWith(new UnlockMaterial.Private(rsaPrv))
.build(false);
dec.setInput(cipherSource);
-
ClassDescriptionReusable opener for
CTX-ENC:<algorithm>entries.Recipient implementation that delegates content-encryption key (CEK) wrapping to a providedEncryptionContext.Reusable opener forKEM:<algorithm>:GCM-WRAPentries.Recipient implementation that derives a key-encryption key (KEK) via aKemContextencapsulation and uses it to wrap a content-encryption key (CEK).Represents one-shot multi-recipient content that owns temporary recipient resources until processing or explicit cleanup.Builds a data content pipeline that supports multiple recipients and delegates payload encryption to AES or ChaCha builders.Recipient opener for password-based entries that derives a KEK via PBKDF2 and unwraps the CEK with AES-GCM.Password recipient that derives a KEK via PBKDF2(HMAC-SHA-256) and wraps the CEK with AES-GCM.Recipient describes how to encode a single recipient entry that can recover a content-encryption key (CEK) during decryption.Defines the KEK sizes supported by recipient entries without an encoded size discriminator.RecipientOpener attempts to recover a content-encryption key (CEK) from a single recipient entry during decryption.Caller-owned session-operation input used to unlock a recipient entry.Destroyable password unlocking material backed by an owned character array.Private key unlocking material.