feat(aead): extract a reusable CipherTree codec for structured ciphers#195
Draft
coderdan wants to merge 2 commits into
Draft
feat(aead): extract a reusable CipherTree codec for structured ciphers#195coderdan wants to merge 2 commits into
coderdan wants to merge 2 commits into
Conversation
coderdan
commented
Jun 8, 2026
Comment on lines
+94
to
+98
| let mut out = Vec::with_capacity(items.len()); | ||
| for item in items { | ||
| out.push(item.try_map_leaves(f)?); | ||
| } | ||
| CipherTree::Sequence(out) |
Contributor
Author
There was a problem hiding this comment.
Self-review: could this be done with a combinator/iterator instead?
Contributor
Author
There was a problem hiding this comment.
Done in 652695e — the Sequence arm is now:
CipherTree::Sequence(items) => CipherTree::Sequence(
items
.into_iter()
.map(|item| item.try_map_leaves(f))
.collect::<Result<Vec<_>, E>>()?,
),collect::<Result<_, _>>() short-circuits on the first Err exactly like the old for + ?, so behaviour is identical.
coderdan
commented
Jun 8, 2026
Comment on lines
+101
to
+106
| let mut out = Vec::with_capacity(entries.len()); | ||
| for (k, v) in entries { | ||
| out.push((k, v.try_map_leaves(f)?)); | ||
| } | ||
| CipherTree::Map(out) | ||
| } |
Contributor
Author
There was a problem hiding this comment.
Same — the Map arm in 652695e:
CipherTree::Map(entries) => CipherTree::Map(
entries
.into_iter()
.map(|(k, v)| v.try_map_leaves(f).map(|v| (k, v)))
.collect::<Result<Vec<_>, E>>()?,
),The .map(|v| (k, v)) re-pairs each transformed value with its (cleartext) key before collecting into the Result.
5279c64 to
51a3fb3
Compare
652695e to
4eda233
Compare
51a3fb3 to
0dfde9b
Compare
4eda233 to
32d5142
Compare
0dfde9b to
1cee66d
Compare
32d5142 to
927299a
Compare
d7f2e9e to
ee7419f
Compare
The structural drive and visitor walk for a recursive ciphertext (single value, sequence, map, option, passthrough) were identical between Aes256Cipher and any other cipher producing the same shape — only the per-leaf seal/open differs. Factor that out into vitaminc-aead: - CipherTree<L>: the recursive container, generic over the leaf payload. - LeafSealer / LeafOpener: the two small per-cipher operations. - TreeCipher<S> / TreeDecipher<O>: generic Cipher/Decipher (plus TreeSeq/TreeMap and the SeqAccess/MapAccess drivers) that do the whole structural traversal. These are generic impls on the concrete local types TreeCipher/TreeDecipher, not a blanket impl over &C — a conditional blanket would conflict (E0119) with the direct Cipher/Decipher impls those traits exist to allow. Aes256Cipher is refactored onto it: AesCipherText is now CipherTree<LocalCipherText>, AesDecipher is TreeDecipher<AesOpener>, and the hand-written Seq/Map ciphers, Decipher impl, and access structs are gone (~290 fewer lines). Behaviour is unchanged — the full encrypt suite passes on both the aws-lc-rs and RustCrypto backends, including the map-contract, option-shape, passthrough, AAD, and nonce-uniqueness tests. This is the shared foundation a ZeroKMS-backed cipher (cipherstash-suite stack-encrypt) can build on, sealing each leaf under its own data key while reusing all of the structural machinery here.
…reuse The `LeafSealer` seam sealed each leaf eagerly during the encrypt traversal, behind a `Copy` bound. That fits an eager single-key cipher but not a batching one (e.g. ZeroKMS), which must collect all leaves, fetch data keys in one request, then seal — it could only satisfy the old seam with a no-op `seal` and a second pass anyway. Split structure from sealing so one codec serves both: - `TreeCipher`/`TreeSeq`/`TreeMap` are now a non-generic structural codec that collects each byte leaf (plaintext + bound AAD) into a `CipherTree<PendingLeaf>` without sealing. `LeafSealer` is removed. - Sealing is a separate per-cipher step. `Aes256Cipher` seals the collected tree via `try_map_leaves` (eager, fresh nonce per leaf); a batch cipher can size a key request with `leaf_count` and seal the same way. `value.encrypt(&cipher)` is unchanged for callers. - `LeafOpener` is kept (decrypt openers are stateless per-leaf lookups, so `Copy` is correct there) and its three recursion sites now use `TreeDecipher::new`. Also: - Add `Nonce::as_array` so `seal` copies the nonce bytes without the previously-dead fallible slice conversion (it needs the nonce twice: for the AEAD call and to append to the ciphertext). - Pin the leaf-ordering contract (`leaf_count`/`for_each_leaf`/ `try_map_leaves` agree on set and order) with a test. - Rewrite `try_map_leaves` seq/map arms as iterator combinators. - Document `CipherTree::None`'s per-leaf key cost and `Passthrough`'s non-serializable type erasure for the future batch cipher.
2fb6233 to
c93f2f1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Factors the recursive ciphertext structure out of
Aes256Cipherinto reusable, crypto-agnostic machinery invitaminc-aead, so any cipher producing the same shape (single value / sequence / map / option / passthrough) reuses it and supplies only its per-leaf seal/open.New in
vitaminc-aead:CipherTree<L>— the recursive container, generic over the leaf payloadL.LeafSealer/LeafOpener— the two small per-cipher operations.TreeCipher<S>/TreeDecipher<O>(+TreeSeq/TreeMapand the seq/map access drivers) — genericCipher/Decipherimpls that perform the whole structural traversal.Aes256Cipheris refactored onto it:AesCipherText = CipherTree<LocalCipherText>,AesDecipher = TreeDecipher<AesOpener>, and the hand-writtenSeq/Mapciphers,Decipherimpl, and access structs are deleted (~290 fewer lines).Why
The structural drive/walk was duplicated verbatim between
Aes256Cipherand the ZeroKMS-backed cipher being built in cipherstash-suite (stack-encrypt). This is the shared foundation that cipher can build on — sealing each leaf under its own data key while reusing everything here.Design note: why not a blanket impl
The generic impls are on the concrete local types
TreeCipher/TreeDecipher, notimpl<C: LeafSealer> Cipher for &C. A conditional blanket would conflict (E0119) with any directCipher/Decipherimpl — and those traits are public extension points meant to be implemented directly (e.g. bystack-encrypt, and by the test doubles here). Concrete ciphers wrap themselves in aLeafSealer/LeafOpenerinstead.Verification
vitaminc-encryptsuite passes on both the aws-lc-rs and RustCrypto (_test-rust-crypto-backend) backends — incl. the map-contract, option-shape, passthrough, AAD-mismatch, and nonce-uniqueness tests.vitaminc-aead42 unit + 8 doctests;cargo build --workspace --all-features✅; clippy-D warnings✅;fmt --check✅.Stacking