feat(cachet): add encrypt feature for authenticated value encryption#558
feat(cachet): add encrypt feature for authenticated value encryption#558schgoo wants to merge 1 commit 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.
| pub(crate) pre: Pre, | ||
| pub(crate) post: Post, | ||
| pub(crate) key_encoder: Box<dyn Encoder<K, KT>>, | ||
| pub(crate) value_codec: Box<dyn Codec<V, VT>>, | ||
| pub(crate) clock: Clock, | ||
| pub(crate) telemetry: CacheTelemetry, | ||
| pub(crate) stampede_protection: bool, | ||
| pub(crate) _phantom: PhantomData<(K, V, KT, VT)>, |
| // 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) => { |
| async fn insert(&self, key: BytesView, entry: CacheEntry<BytesView>) -> Result<(), Error> { | ||
| let aad = key.to_vec(); | ||
| let encrypted = entry.try_map_value(|value| self.cipher.encrypt(&aad, &value))?; | ||
| self.inner.insert(key, encrypted).await | ||
| } |
Codecov Report❌ Patch coverage is
❌ Your project check has failed because the head coverage (99.8%) is below the target coverage (100.0%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #558 +/- ##
========================================
- Coverage 100.0% 99.8% -0.2%
========================================
Files 356 358 +2
Lines 26966 27237 +271
========================================
+ Hits 26966 27187 +221
- Misses 0 50 +50 ☔ 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.
Summary
Adds an optional
encryptfeature tocachetthat encrypts cache valueswith AES-256-GCM before they reach a fallback/remote tier. Values are
cryptographically bound to their storage key, so a value cannot be read back
under a different key. Enabled via a single builder method:
Motivation
When a cache tiers down to an untrusted store (Redis, S3, etc.), values are
exposed at rest. This feature lets callers keep the ergonomic typed cache API
while transparently encrypting values on the way out and decrypting them on the
way back, without hand-rolling a serialization/encryption pipeline.
What it does
.encrypt(&key)— available after.serialize()(once values areBytesView). Encrypts each value with AES-256-GCM using a fresh random nonce.(AAD), so a ciphertext produced for one key fails to decrypt under any other
key. This prevents an attacker with write access to the backing store from
relocating or swapping ciphertexts between keys.
left serialized-but-unencrypted to remain deterministic and lookupable.
(corrupt, truncated, wrong key, tampered, or relocated) is treated as a cache
miss (
Ok(None)) and recomputed, consistent with the existing serializationcodec'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()returns a dedicatedEncryptedTransformBuilder(storage typesfixed to
BytesView), which supports.fallback()and.build(), mirroringTransformBuilder. It lives in its ownbuilder/encrypt.rs, matching theserializefeature's file layout.AeadCiphertrait abstracts the cipher (Aes256GcmCipheris theonly implementation). It is intentionally crate-private for now; the public
surface is just
.encrypt(&key). The seam can be exposed later without abreaking change if pluggable ciphers are needed.
BytesViewinto a contiguousbuffer exactly once per direction (encrypt-in-place with a detached tag on the
way out; borrow-or-gather on the way in), avoiding redundant copies.
Dependencies
Adds
aes-gcmandgetrandomto the workspace, pulled in only under theoptional
encryptfeature. Licenses verified viacargo deny check licenses.Testing
wrong-key / wrong-AAD / tampered / too-short soft-failures, multi-span
plaintext and ciphertext handling, and that
Debugnever renders key material..serialize().encrypt().fallback()pipeline:stored bytes are ciphertext (not plaintext), round-trips through the encrypted
tier, fresh nonce per insert, reachable on a
FallbackBuilder, and aciphertext relocated to a different key reads as a miss.
Notes / follow-ups (not in this PR)
telemetry; emitting an event on decrypt soft-failure (to make tampering
observable) is a possible follow-up.
random-nonce birthday bound) and an explicit note that cache keys are stored in
plaintext, so secrets/PII should not be placed in keys.