feat(aead): keep byte leaves inside Protected end-to-end; derive Encrypt for Key and tagged leaves - #295
Open
coderdan wants to merge 3 commits into
Open
feat(aead): keep byte leaves inside Protected end-to-end; derive Encrypt for Key and tagged leaves#295coderdan wants to merge 3 commits into
coderdan wants to merge 3 commits into
Conversation
…rypt `Protected<T>`'s blanket `Encrypt`/`Decrypt` impls unwrapped to a bare `T` and relied on the leaf impl re-wrapping it, so a `Protected<[u8; 32]>` was copied onto the stack as a bare array on its way to a cipher that already accepts it wrapped. That window is why `Key` and the FFI tagged leaves carried hand-written impls instead of deriving. Add `Encrypt::encrypt_protected` and `Decrypt::decrypt_protected` — default methods the blanket impls route through — and override them on the byte leaves (`[u8; N]`, `Vec<u8>`) to hand the still-wrapped value straight to `encrypt_bytes_array` / `encrypt_bytes_vec`, and to keep the decipher's `Protected<Vec<u8>>` intact on the way out. `[u8; N]` decryption now copies out of the wrapped buffer rather than `try_into`, which dropped the vector unwiped. `Vec<u8>` gains the `Encrypt` leaf it was missing: it already decrypted as a byte leaf, but could not be encrypted at all, since `u8` is not `Encrypt`. Document the seam in the README and fold `#[aead(passthrough)]` into the "custom types" guidance now that the derive covers cleartext fields. Claude-Session: https://claude.ai/code/session_01KGhQ75hjeeBam4HBjwPppL
…leaves With `Protected<[u8; N]>` and `Protected<Vec<u8>>` reaching the cipher still wrapped via `encrypt_protected`, a transparent derived newtype seals byte-for-byte as the hand-written impls did, with the same chain of custody. Replace the three impls with `#[derive(Encrypt)]` and pin the transparency, the wrapped/bare wire interchange, and `Key`'s raw-bytes shape against the real AES-256-GCM cipher. Claude-Session: https://claude.ai/code/session_01KGhQ75hjeeBam4HBjwPppL
Contributor
🧬 Mutation testing (cargo-mutants,
|
| caught | missed | unviable | timeout |
|---|---|---|---|
| 3 | 0 | 55 | 0 |
✅ Every mutant in the changed lines was caught by a test.
Contributor
✅ No CRAP threshold violations541 function(s) analyzed · threshold 30 |
coderdan
commented
Aug 28, 2026
| } | ||
| ``` | ||
|
|
||
| But if `id` and `email` are not to be stored in this ciphertext at all, that is a hand-written impl: |
Contributor
Author
There was a problem hiding this comment.
That indicates that we should have a skip option as well.
cargo-mutants runs only the mutated crate's tests, so the three mutants in `array_from_protected` (body replaced with a constant array, `!=` flipped to `==`) survived: the tests that exercised it lived in `vitaminc-encrypt` behind a real cipher. These unit tests drive the bare and wrapped `[u8; N]` decrypt paths through `MockDecipher` so the copy and the length rejection are both caught where the logic lives. Claude-Session: https://claude.ai/code/session_01KGhQ75hjeeBam4HBjwPppL
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.
Follow-up to #287. A sweep for hand-written
Encrypt/Decryptimpls that the new derive could replace found none that should be — but the reason was a gap invitaminc-aead, not in the derive.The gap
Protected<T>'s blanket impls didrisky_unwrap()and let the leaf re-wrap, so aProtected<[u8; 32]>was briefly a bare array on the stack on its way to a cipher that already takes it wrapped (encrypt_bytes_array).Key,TaggedFixedandTaggedVariableall hand-wroteEncryptpurely to sidestep that.TaggedVariablecouldn't derive at all:Vec<u8>had noEncryptleaf (onlyDecrypt).The fix
Encrypt::encrypt_protected(this: Protected<Self>, …)andDecrypt::decrypt_protected(…) -> D::Ok<Protected<Self>>, default methods the blanketProtected<T>impls now route through. Defaults unwrap as before; the byte leaves ([u8; N],Vec<u8>) override them to hand the wrapped value straight to the cipher / keep the decipher'sProtected<Vec<u8>>intact. No specialization needed, no wire change.Vec<u8>: Encryptbyte leaf (coherence permits it besideVec<T: Encrypt>for the same reason theDecryptside already compiles).[u8; N]decrypt copies out of the wrapped buffer instead ofVec::try_into, which dropped the vector unwiped.Key,TaggedFixed,TaggedVariablenow#[derive(Encrypt)]— byte-identical wire, same custody, pinned bypackages/encrypt/tests/protected_bytes.rsagainst real AES-256-GCM.#[aead(passthrough)]and lists what still needs a hand-written impl.Not in this PR
Equatable<T>still unwraps to the innermost value: it has no way to peel only its own layer and pass theProtected<T>beneath on intact. Needs a smallvitaminc-protectedAPI. Comment updated to say so.#[aead(skip)]for fields left out of the ciphertext, and fieldless-enum support — derive-level gaps surfaced by the same sweep, tracked separately.Verified:
cargo test --workspace --all-features(kms needs localstack, fails on main too),cargo clippy --all-targets --all-features -D warnings,RUSTDOCFLAGS=-D warnings cargo doc --workspace --all-features --no-deps,cargo fmt --check.https://claude.ai/code/session_01KGhQ75hjeeBam4HBjwPppL