feat(cachet): add encrypt feature for authenticated value encryption#558
feat(cachet): add encrypt feature for authenticated value encryption#558schgoo wants to merge 12 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 362 +2
Lines 27886 28197 +311
========================================
+ Hits 27886 28197 +311 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
|
||
| use std::borrow::Cow; | ||
|
|
||
| use aes_gcm::aead::{Aead, AeadInPlace, KeyInit, Payload}; |
There was a problem hiding this comment.
From a compliance perspective, should this code really come from that crate? Would love to hear Sergey's input here.
There was a problem hiding this comment.
I've converted it to take an AES cipher implementation as input, so we don't have to be opinionated about the crypto library. But I also added a symcrypt implementation behind a feature gate, rather than use aes_gcm.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| use std::sync::atomic::{AtomicU32, Ordering}; | ||
|
|
||
| use bytesbuf::BytesView; | ||
| use cachet::{AeadCipher, Cache, CacheEntry, CacheOp, CacheTier, DecodeOutcome, Error, MockCache}; |
Summary
Adds an optional
encryptfeature tocachetthat encrypts 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 encryption mechanism and carries no cryptographic
dependency of its own — callers plug in a cipher backed by their approved
cryptographic library via
.encrypt_with(cipher):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 encrypting values on the way out and decrypting 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
.encrypt_with(cipher)— available after.serialize()(once values areBytesView). Encrypts each value with a caller-suppliedAeadCipher.a ciphertext produced for one key fails to decrypt under any other key,
preventing an attacker with write access to the backing store from relocating
or swapping ciphertexts 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.decrypt_failedtelemetry event sotampering is observable, consistent with the serialization codec's
soft-failure behavior.
Design
EncryptedTierinstalled at the storageboundary, where both the key and value are in scope — this is what makes
key-as-AAD binding possible.
.encrypt_with()returns a dedicatedEncryptedTransformBuilder(storage typesfixed to
BytesView) supporting.fallback()and.build(), mirroringTransformBuilder. It lives in its ownbuilder/encrypt.rs, matching theserializefeature's file layout.AeadCiphertrait is the pluggable seam; the crate ships noimplementation. A complete reference
AeadCipherbacked by SymCrypt(FIPS-certifiable AES-256-GCM) is included in the crate-level docs as a
copy-paste example, rather than as a compiled feature — SymCrypt needs a native
library at build/run time, which would force it onto every consumer and CI job.
Dependencies
None. The
encryptfeature adds no cryptographic dependency; consumers bringtheir own approved library.
Testing
.serialize().encrypt_with().fallback()pipeline through a crypto-free dummy
AeadCipher(nonce + AAD authentication +a reversible keystream transform): stored bytes are ciphertext with the
plaintext never appearing verbatim, values round-trip through the encrypted
tier, a fresh nonce is used per insert, the boundary is reachable on a
FallbackBuilderand through chained post-transform fallbacks, and a ciphertextrelocated to a different key reads as a miss.