feat(cachet): add encrypt feature for authenticated value encryption#558
feat(cachet): add encrypt feature for authenticated value encryption#558schgoo wants to merge 18 commits into
encrypt feature for authenticated value encryption#558Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional encrypt feature to cachet that introduces an authenticated encryption boundary for cache values (AES-256-GCM) before data reaches an untrusted fallback tier, binding ciphertext to the storage key via GCM AAD.
Changes:
- Introduces
AeadCipher/Aes256GcmCipherand anEncryptedTierthat encrypts values and treats undecryptable entries as cache misses. - Adds
.encrypt(&[u8; 32])to the serialized builder pipeline viaEncryptedTransformBuilder, supporting fallback chaining andbuild(). - Adds unit + integration tests, docs/README updates, and feature-gated dependencies (
aes-gcm,getrandom).
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/cachet/tests/encrypt.rs | Integration tests for .serialize().encrypt().fallback() behavior and key-binding relocation defense. |
| crates/cachet/src/transform/mod.rs | Wires in the encrypt transform module behind the encrypt feature gate. |
| crates/cachet/src/transform/encrypt.rs | Implements AES-256-GCM cipher + EncryptedTier wrapper for value encryption/decryption at the storage boundary. |
| crates/cachet/src/lib.rs | Documents the new encrypt feature and re-exports EncryptedTransformBuilder when enabled. |
| crates/cachet/src/builder/transform.rs | Adjusts TransformBuilder field visibility to enable the .encrypt() builder transition. |
| crates/cachet/src/builder/mod.rs | Adds the encrypt builder module and exports EncryptedTransformBuilder behind the feature. |
| crates/cachet/src/builder/encrypt.rs | Implements .encrypt(&key) and the EncryptedTransformBuilder fallback/build pipeline. |
| crates/cachet/README.md | Regenerated README content to document the new feature. |
| crates/cachet/Cargo.toml | Adds the encrypt feature and its optional deps. |
| Cargo.toml | Adds workspace dependency entries for aes-gcm and getrandom. |
| Cargo.lock | Locks new crypto/randomness transitive dependencies. |
| .spelling | Adds crypto-related terminology used by new docs/tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // The storage key is authenticated as AAD, so a value planted under the | ||
| // wrong key fails decryption and is treated as a miss. | ||
| match self.cipher.decrypt(&key.to_vec(), &value)? { | ||
| DecodeOutcome::Value(value) => { |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #558 +/- ##
=========================================
Coverage 100.0% 100.0%
=========================================
Files 360 396 +36
Lines 28002 33846 +5844
=========================================
+ Hits 28002 33846 +5844
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…kValueProtector under test-util. Make trait more generic - not aead-specific, but including additional context parameter that can be used that way.
| #![cfg(all(feature = "encrypt", feature = "serialize", feature = "test-util"))] | ||
|
|
||
| use bytesbuf::BytesView; |
Summary
Adds an optional
encryptfeature tocachetthat protects cache valuesbefore they reach a fallback/remote tier, binding each value to its storage key so
it cannot be read back under a different key.
The feature ships only the protection mechanism and carries no cryptographic
dependency of its own — callers plug in a protector backed by their approved
cryptographic library via
.protect_with(protector):Motivation
When a cache tiers down to an untrusted store (Redis, S3, etc.), values are
exposed at rest. This feature keeps the ergonomic typed cache API while
transparently protecting values on the way out and recovering them on the way
back, with no hand-rolled serialization/encryption pipeline. Shipping the
mechanism without any bundled crypto lets each consumer satisfy its own
cryptographic-library compliance requirements and keeps the crate dependency-free
and portable across all CI targets.
What it does
.protect_with(protector)— available after.serialize()(once values areBytesView). Protects each value with a caller-suppliedValueProtector.ValueProtectortrait — the pluggableprotect(context, plaintext)/unprotect(context, protected)contract. The verb pair mirrors OSdata-protection APIs (Windows DPAPI
CryptProtectData, .NETIDataProtector);contextis the AEAD-associated-data / DPAPI-entropy role.contextand must be bound,so a value protected for one key fails to recover under any other key. This
prevents an attacker with write access to the backing store from relocating or
swapping values between keys.
deterministic and lookupable, so secrets/PII must not be placed in cache keys.
(corrupt, truncated, wrong key, tampered, or relocated) is treated as a cache
miss (
Ok(None)) and emits acache.unprotect_failedtelemetry event (taggedfallback = true, since the protected tier always sits on the fallback side) sotampering is observable, consistent with the serialization codec's soft-failure
behavior.
Design
ProtectedTierinstalled at the storageboundary, where both the key and value are in scope — this is what makes the
key-as-context binding possible.
.protect_with()returns a dedicatedProtectedTransformBuilder(storage typesfixed to
BytesView) supporting.fallback()and.build(), mirroringTransformBuilder. It lives in its ownbuilder/encrypt.rs, matching theserializefeature's file layout.ValueProtectortrait is the pluggable seam; the crate provides nocryptographic implementation. A complete reference
ValueProtectorbacked bySymCrypt (FIPS-certifiable AES-256-GCM) is documented in the crate-level docs as
a copy-paste example, since SymCrypt requires a native library at build/run time
and pulling it into the workspace would force it onto all
--all-featuresCI.encryptfeature namesthe capability (discoverable; avoids overloading the existing
stampede_protection), while the API speaksprotect/unprotect— the same way.NET's
DataProtectionpackage exposes anIDataProtector.Protectmethod.Testability
Per the Microsoft Pragmatic Rust Guidelines (M-MOCKABLE-SYSCALLS,
M-DESIGN-FOR-AI, M-TEST-UTIL), the crate abstracts the non-deterministic
crypto/entropy work behind the
ValueProtectorseam and ships a mock sodownstream consumers can test their protected-cache pipelines without a crypto
dependency:
MockValueProtector— a deterministic, crypto-freeValueProtectorgatedbehind
test-utiland re-exported at the crate root next toMockCache. Itbinds
contextand soft-fails on mismatch/corruption, but provides noconfidentiality (documented as test-only).
Dependencies
None. The
encryptfeature adds no cryptographic dependency; consumers bringtheir own approved library.
Testing
ProtectedTiermechanism and theMockValueProtector..serialize().protect_with().fallback()pipeline via
MockValueProtector: stored bytes are ciphertext with theplaintext never appearing verbatim, values round-trip through the protected tier,
a fresh nonce is used per insert, the boundary is reachable on a
FallbackBuilderand through chained post-transform fallbacks, and a value relocated to a
different key reads as a miss.
transform/encrypt.rsandbuilder/encrypt.rs.