Skip to content

feat(cachet): add encrypt feature for authenticated value encryption#558

Open
schgoo wants to merge 18 commits into
mainfrom
cachet_encrypt
Open

feat(cachet): add encrypt feature for authenticated value encryption#558
schgoo wants to merge 18 commits into
mainfrom
cachet_encrypt

Conversation

@schgoo

@schgoo schgoo commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an optional encrypt feature to cachet that protects cache values
before 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):

let cache = Cache::builder::<String, String>(clock)
    .memory()
    .serialize()
    .protect_with(my_protector)   // any `ValueProtector` implementation
    .fallback(remote)
    .build();

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 are
    BytesView). Protects each value with a caller-supplied ValueProtector.
  • ValueProtector trait — the pluggable protect(context, plaintext) /
    unprotect(context, protected) contract. The verb pair mirrors OS
    data-protection APIs (Windows DPAPI CryptProtectData, .NET IDataProtector);
    context is the AEAD-associated-data / DPAPI-entropy role.
  • Key binding — the storage key is passed as the context and 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.
  • Keys are not protected — they stay serialized-but-plaintext to remain
    deterministic and lookupable, so secrets/PII must not be placed in cache keys.
  • Unrecoverable entries read as a miss — a value that fails authentication
    (corrupt, truncated, wrong key, tampered, or relocated) is treated as a cache
    miss (Ok(None)) and emits a cache.unprotect_failed telemetry event (tagged
    fallback = true, since the protected tier always sits on the fallback side) so
    tampering is observable, consistent with the serialization codec's soft-failure
    behavior.

Design

  • Protection is applied by an internal ProtectedTier installed at the storage
    boundary, where both the key and value are in scope — this is what makes the
    key-as-context binding possible.
  • .protect_with() returns a dedicated ProtectedTransformBuilder (storage types
    fixed to BytesView) supporting .fallback() and .build(), mirroring
    TransformBuilder. It lives in its own builder/encrypt.rs, matching the
    serialize feature's file layout.
  • The public ValueProtector trait is the pluggable seam; the crate provides no
    cryptographic implementation. A complete reference ValueProtector backed by
    SymCrypt (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-features CI.
  • Naming follows a capability-vs-mechanism split: the encrypt feature names
    the capability (discoverable; avoids overloading the existing
    stampede_protection), while the API speaks protect/unprotect — the same way
    .NET's DataProtection package exposes an IDataProtector.Protect method.

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 ValueProtector seam and ships a mock so
downstream consumers can test their protected-cache pipelines without a crypto
dependency:

  • MockValueProtector — a deterministic, crypto-free ValueProtector gated
    behind test-util and re-exported at the crate root next to MockCache. It
    binds context and soft-fails on mismatch/corruption, but provides no
    confidentiality
    (documented as test-only).

Dependencies

None. The encrypt feature adds no cryptographic dependency; consumers bring
their own approved library.

Testing

  • Unit tests for the ProtectedTier mechanism and the MockValueProtector.
  • Integration tests drive the full .serialize().protect_with().fallback()
    pipeline via MockValueProtector: stored bytes are ciphertext with the
    plaintext never appearing verbatim, values round-trip through the protected tier,
    a fresh nonce is used per insert, the boundary is reachable on a FallbackBuilder
    and through chained post-transform fallbacks, and a value relocated to a
    different key reads as a miss.
  • 100% line and region coverage on the added transform/encrypt.rs and
    builder/encrypt.rs.

Copilot AI review requested due to automatic review settings July 9, 2026 18:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/Aes256GcmCipher and an EncryptedTier that encrypts values and treats undecryptable entries as cache misses.
  • Adds .encrypt(&[u8; 32]) to the serialized builder pipeline via EncryptedTransformBuilder, supporting fallback chaining and build().
  • 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.

Comment thread crates/cachet/src/builder/transform.rs Outdated
Comment thread crates/cachet/src/transform/encrypt.rs Outdated
Comment on lines +197 to +200
// 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) => {
Comment thread crates/cachet/src/transform/encrypt/tier.rs
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (958d222) to head (0389c64).
⚠️ Report is 1 commits behind head on main.

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     
Flag Coverage Δ
linux 63.6% <100.0%> (?)
linux-arm 67.1% <100.0%> (?)
windows 65.9% <100.0%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread crates/cachet/src/transform/encrypt.rs Outdated

@ralfbiedert ralfbiedert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Adding a blocker for now until we know more about that)

Copilot AI review requested due to automatic review settings July 14, 2026 16:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.

Comment thread crates/cachet/src/transform/symcrypt_cipher.rs Outdated
Comment thread crates/cachet/Cargo.toml Outdated
Copilot AI review requested due to automatic review settings July 15, 2026 16:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comment thread crates/cachet/src/builder/encrypt.rs Outdated
Comment thread crates/cachet/src/telemetry/cache.rs Outdated
Comment thread crates/cachet/tests/encrypt.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment thread crates/cachet/src/telemetry/cache.rs
Comment thread crates/cachet/src/builder/encrypt.rs Outdated
Copilot AI review requested due to automatic review settings July 15, 2026 18:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread crates/cachet/src/builder/encrypt.rs Outdated
Copilot AI review requested due to automatic review settings July 15, 2026 21:10
@schgoo
schgoo enabled auto-merge (squash) July 15, 2026 21:10
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread crates/cachet/tests/encrypt.rs Outdated
@ralfbiedert
ralfbiedert dismissed their stale review July 16, 2026 16:26

crypto removed

schgoo added 2 commits July 22, 2026 11:17
…kValueProtector under test-util. Make trait more generic - not aead-specific, but including additional context parameter that can be used that way.
Copilot AI review requested due to automatic review settings July 22, 2026 15:25
Comment thread crates/cachet/src/transform/encrypt.rs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread crates/cachet/src/builder/encrypt.rs
Copilot AI review requested due to automatic review settings July 22, 2026 16:12
Comment thread crates/cachet/src/transform/encrypt.rs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 22, 2026 18:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment thread crates/cachet/src/transform/encrypt.rs Outdated
Comment thread crates/cachet/src/transform/encrypt.rs Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 22:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comment on lines +10 to +12
#![cfg(all(feature = "encrypt", feature = "serialize", feature = "test-util"))]

use bytesbuf::BytesView;
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.

4 participants