Skip to content

refactor(aead)!: thread AAD through the decrypt path to mirror encrypt#191

Open
coderdan wants to merge 4 commits into
fix/protected-zeroize-on-dropfrom
feat/decipher-mirror-cipher
Open

refactor(aead)!: thread AAD through the decrypt path to mirror encrypt#191
coderdan wants to merge 4 commits into
fix/protected-zeroize-on-dropfrom
feat/decipher-mirror-cipher

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented Jun 6, 2026

What & why

Makes the decrypt path symmetric with the encrypt path. Today AAD is baked into the concrete AesDecipher at construction, while encryption threads it per-call through Cipher/Encrypt. That asymmetry meant there was no generic decrypt entry point and blocked a clean ContextTag::decrypt_with_aad helper.

AAD is now threaded per-call:

  • Decipher::decrypt_bytes/decrypt_seq/decrypt_map/decrypt_option take an aad: A parameter.
  • New Decrypt::decrypt_with_aad — the dual of Encrypt::encrypt_with_aad; Decrypt::decrypt now defaults to it with Aad::empty().
  • Aes256Cipher::decrypt_with_aad delegates to T::decrypt_with_aad (the AesDecipher no longer carries an aad field); AesSeqAccess/AesMapAccess re-supply their stored AAD per element.

Deliberately kept

The visitor pattern (DecipherVisitor / SeqAccess / MapAccess), the Ok<T> GAT, and the infallible map_ok are retained — they are the correct pull-side duals of the encrypt-side builder, not incidental indirection. Per-element AAD rides on the Decipher methods so SeqAccess/MapAccess signatures are untouched.

Breaking change

Breaking to the Decipher/Decrypt traits. These are unreleased (v0.2.0-pre), so there are no external consumers. The public cipher.decrypt(ct) / cipher.decrypt_with_aad(ct, aad) entry points are unchanged.

Verification

  • cargo test -p vitaminc-aead -p vitaminc-encrypt — 29 + 52 unit, all doctests, all ~40 roundtrip/negative cipher tests (incl. wrong-AAD, wrong-shape, option-tag-auth) pass.
  • cargo build --workspace, cargo test -p vitaminc-aead --all-features (hlist), cargo fmt --check all pass.

Follow-up

Unblocks a ContextTag::decrypt_with_aad helper built on Decrypt::decrypt_with_aad (separate PR).

@coderdan coderdan force-pushed the feat/decipher-mirror-cipher branch from a72d591 to 289cabf Compare June 6, 2026 09:25
@coderdan coderdan changed the base branch from main to fix/protected-zeroize-on-drop June 6, 2026 09:25
coderdan added 3 commits June 6, 2026 17:37
Make decryption symmetric with encryption. AAD is now passed per-call
through the Decipher methods and a new Decrypt::decrypt_with_aad entry
point (the dual of Encrypt::encrypt_with_aad), instead of being baked
into the concrete decipher at construction. Aes256Cipher::decrypt_with_aad
now delegates to T::decrypt_with_aad.

The visitor pattern (DecipherVisitor / SeqAccess / MapAccess) and the
Ok<T> GAT are retained deliberately as the correct pull-side duals of the
encrypt-side builder; map_ok stays infallible.

Unblocks a follow-up ContextTag::decrypt_with_aad helper. Breaking change
to the Decipher/Decrypt traits; pre-release, so no external consumers.
- Fix the map_ok doc example to override decrypt_with_aad and thread aad
  (the old example showed decrypt/T::decrypt, which would silently drop AAD
  in a wrapper-type impl).
- Document on SeqAccess::next_element / MapAccess::next_entry that
  implementations must authenticate each element/value against the
  sequence/map AAD, surfacing the contract the encrypt side carries in
  SeqCipher::encrypt_next's signature.
- Store the AAD in AesSeqAccess/AesMapAccess as Aad (copy-on-write) instead
  of an owned Vec<u8>, so a borrowed AAD stays borrowed and the per-element
  clone is cheap; also drops the duplicated .as_bytes().to_vec() lowering.
next_element/next_entry passed self.aad.clone(), which deep-copies the
bytes once per element when the AAD is owned (String/Vec<u8>/u64) — N copies
for an N-element sequence. Pass the borrowed bytes (&[u8]: IntoAad) instead;
the borrow only needs to live for the synchronous element decrypt. Combined
with the Aad field (owned AAD moves in zero-copy), the seq/map decrypt path
is now allocation-free with respect to AAD.
@coderdan coderdan force-pushed the feat/decipher-mirror-cipher branch from 289cabf to d6a15d0 Compare June 6, 2026 09:37
@coderdan coderdan marked this pull request as ready for review June 6, 2026 09:38
Make AesDecipher public and add Aes256Cipher::decipher(ct) so callers can
obtain a concrete Decipher and drive Decrypt::decrypt_with_aad directly —
the decrypt-side counterpart to handing out &cipher (a Cipher). The inherent
decrypt/decrypt_with_aad now delegate to it. Unblocks generic decrypt-side
helpers (e.g. ContextTag::decrypt_with_aad) without a separate decryptor trait.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant