Skip to content

feat(encryption): keyring rotation — previous_master_keys + sequential decrypt (LAB-686) - #63

Merged
27Bslash6 merged 6 commits into
mainfrom
lab-686-keyring-rotation
Aug 8, 2026
Merged

feat(encryption): keyring rotation — previous_master_keys + sequential decrypt (LAB-686)#63
27Bslash6 merged 6 commits into
mainfrom
lab-686-keyring-rotation

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Stage 2 (rs) of the LAB-516 key-rotation train. Closes LAB-686.

Problem

cachekit-rs had no rotation surface: the config builder had .master_key(hex) and nothing else, so rotating the master key invalidated every encrypted entry.

What this adds

An operator can now configure up to 3 decrypt-only previous master keys and read entries written under any of them without re-encrypting — per protocol/decisions/key-rotation.md and protocol/spec/encryption.md → "Key Rotation (Keyring)".

  • Config surface: .previous_master_keys(&[hex, ...]) on CachekitConfigBuilder (per-entry validation identical to .master_key()) + CACHEKIT_PREVIOUS_MASTER_KEYS env var (comma-separated hex). More than 3 entries → CachekitError::Config, never truncated. Current key appearing in the previous list → rejected at build/load (detectable subset of the forward-only rule).
  • Decrypt path: EncryptionLayer now holds a cachekit_core::Keyring (the shared stage-1 helper from LAB-683, core 0.5.0) — no keyring logic re-implemented in this crate. Sequential attempts, current key first, identical AAD per attempt. rs entries carry no per-entry key identity, so the sequential branch is the spec-assigned one (spec L369); no fingerprint selection added, by design.
  • Writes unchanged: always encrypt under the current key (keyring is read-side only).
  • Client: CacheKitBuilder::encryption_from_bytes_with_previous(...) + from_env wiring.
  • Core pin: cachekit-core 0.4 → 0.5 (Keyring ships in 0.5.0).
  • Docs: builder doc-test example, README "Key Rotation" section + CACHEKIT_PREVIOUS_MASTER_KEYS env row.

Tests (all AC covered)

  • Builder + env ingestion, attempt-order preservation
  • Cap of 3 rejected (builder + env), never truncated
  • Self-collision rejected in both builder call orders + env
  • k₁-encrypted value decrypts with master=k₂, previous=[k₁]; fails with previous=[]
  • End-to-end rotation round-trip against a shared mock backend: write under k₁ → rotate → read succeeds with stored bytes byte-identical (no re-encryption) → k₁ dropped → read fails as CachekitError::Encryption
  • Drift guard: config-level cap constant == cachekit_core::MAX_DECRYPT_ONLY_KEYS

Local gate: cargo fmt --check, clippy -D warnings (CI feature set), full test suite incl. doc-tests — all green.

Dependency bump evidence — cachekit-core 0.4 → 0.5 (supply chain)

  • OSV: https://api.osv.dev/v1/query for cachekit-core 0.5.0 (crates.io) returns no known vulnerabilities (empty result, checked 2026-08-08).
  • cargo-deny + cargo-audit: this repo's security.yml workflow (advisories + bans + licenses + sources, --all-features) is green on this PR head — run 31187683200.
  • Lockfile: Cargo.lock resolves cachekit-core 0.5.0 from crates.io (registry checksum pinned by the lockfile).
  • Provenance: cachekit-core is first-party (this org), released via release-please from cachekit-core#67/#68; the 0.5 caret pin matches the repo's existing convention (was 0.4), with the exact version pinned by Cargo.lock.

Out of scope (per ticket)

Fingerprint-based selection, nonce-exhaustion handling, per-tenant derivation changes, rotation runbook + feature-matrix flip (LAB-687, stage 3).

Summary by CodeRabbit

  • New Features

    • Added master-key rotation with a current encryption key and up to three decrypt-only previous keys.
    • Added builder and CACHEKIT_PREVIOUS_MASTER_KEYS environment variable configuration.
    • Added validation for key format, minimum length, duplicates, blank entries and rotation limits.
    • Existing encrypted data remains readable after rotation without re-encryption.
    • New writes always use the current key.
  • Documentation

    • Added configuration examples and documented forward-only rotation behaviour, including rejection of the current key as a previous key.

…equential decrypt (LAB-686)

Rotating the master key previously invalidated every encrypted entry:
the SDK had no rotation surface at all. Implements the stage-2 rs child
of the LAB-516 key-rotation train, per protocol/decisions/key-rotation.md
and spec/encryption.md 'Key Rotation (Keyring)'.

- config: .previous_master_keys(...) builder (hex validation identical
  to .master_key()) + CACHEKIT_PREVIOUS_MASTER_KEYS env (comma-separated
  hex). Cap of 3 rejected never truncated; current-key self-collision
  rejected at build/load (forward-only rotation, detectable subset).
- encryption: EncryptionLayer holds cachekit_core::Keyring (the shared
  stage-1 helper, LAB-683 / core 0.5.0 — no keyring logic re-implemented
  here). Decrypt attempts keys sequentially, current first, identical
  AAD per attempt; rs entries carry no per-entry key identity, so the
  sequential branch is the spec-assigned one (no fingerprint selection).
  Writes always encrypt under the current key.
- client: encryption_from_bytes_with_previous(...) + from_env wiring.
- core pin bumped 0.4 -> 0.5 (Keyring ships in 0.5.0).
- docs: builder doc-test, README key-rotation section + env table row.
  Config-level cap constant mirrors core's (feature-gated) constant;
  drift-guard test asserts equality.
@kodus-27b

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 069ec212-f621-409a-8417-b055c45237a7

📥 Commits

Reviewing files that changed from the base of the PR and between 7e2a98f and 79bee95.

📒 Files selected for processing (2)
  • crates/cachekit/src/config.rs
  • crates/cachekit/tests/config_tests.rs

Walkthrough

Cachekit adds decrypt-only previous master-key support. Configuration accepts up to three previous keys from the environment or builder. Encryption uses the current key for writes and ordered keyring fallback for reads.

Changes

Master-key rotation

Layer / File(s) Summary
Configuration and validation
crates/cachekit/src/config.rs, crates/cachekit/tests/config_tests.rs, README.md
Configuration parses and validates up to three previous hexadecimal keys. Tests cover ordering, limits, invalid keys, empty entries, and current-key overlap. Documentation describes environment and builder usage.
Keyring-backed encryption
crates/cachekit/src/encryption.rs, crates/cachekit/tests/encryption_tests.rs, crates/cachekit/tests/intent_tests.rs, crates/cachekit/Cargo.toml
EncryptionLayer uses Keyring for current-key-first decryption and current-key-only encryption. Tests cover rotation, hard cut-over behaviour, and configuration error classification.
Client and builder integration
crates/cachekit/src/client.rs
Environment loading and encryption_from_bytes_with_previous pass ordered previous keys to EncryptionLayer. A no-encryption compatibility stub preserves builder compilation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant CacheKitBuilder
  participant EncryptionLayer
  participant Keyring
  Application->>CacheKitBuilder: configure current and previous keys
  CacheKitBuilder->>EncryptionLayer: create encryption layer
  EncryptionLayer->>Keyring: initialise ordered keyring
  Application->>EncryptionLayer: write data
  EncryptionLayer-->>Application: ciphertext encrypted with current key
  Application->>EncryptionLayer: read ciphertext
  EncryptionLayer->>Keyring: try current key, then previous keys
  Keyring-->>Application: decrypted data
Loading

Possibly related PRs

Suggested reviewers: kodus-27b

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: encryption keyring rotation with previous master keys and sequential decryption.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lab-686-keyring-rotation

Comment @coderabbitai help to get the list of available commands.

Comment thread crates/cachekit/Cargo.toml
Comment thread crates/cachekit/src/config.rs
Comment thread crates/cachekit/src/encryption.rs

@kodus-27b kodus-27b Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found critical issues please review the requested changes

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/cachekit/src/encryption.rs (1)

85-98: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Validate the current master key before the previous keys, and note the error-variant asymmetry.

Two points about this validation block:

  1. The previous-key length loop runs before the current-key length check. If a caller supplies a short current key together with a short previous key, the returned error names the previous key. The current key is the more fundamental fault and should surface first.
  2. The same defect class returns different variants. A short previous key returns CachekitError::Config. A short current key three lines below returns CachekitError::Encryption. A caller that matches on the variant to distinguish operator configuration faults from crypto faults will misclassify a short current key.

Point 2 is partly pre-existing, since the current-key check kept its original variant. Changing it would break the master_key_too_short expectation. Reordering alone is safe and improves the diagnostic.

♻️ Proposed reorder
     ) -> Result<Self, CachekitError> {
-        for (i, key) in previous_keys.iter().enumerate() {
-            if key.len() < 32 {
-                return Err(CachekitError::Config(format!(
-                    "previous master key {i} must be at least 32 bytes; got {}",
-                    key.len()
-                )));
-            }
-        }
         if master_key_bytes.len() < 32 {
             return Err(CachekitError::Encryption(format!(
                 "master key must be at least 32 bytes; got {}",
                 master_key_bytes.len()
             )));
         }
+        for (i, key) in previous_keys.iter().enumerate() {
+            if key.len() < 32 {
+                return Err(CachekitError::Config(format!(
+                    "previous master key {i} must be at least 32 bytes; got {}",
+                    key.len()
+                )));
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cachekit/src/encryption.rs` around lines 85 - 98, In the validation
block containing the previous_keys loop and master_key_bytes check, move the
current master-key length validation before iterating over previous_keys so a
short current key is reported first when both are invalid. Preserve the existing
CachekitError::Encryption variant and all current error messages; do not change
the previous-key validation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cachekit/src/client.rs`:
- Around line 1013-1019: Update the documentation for the encryption
configuration methods around EncryptionLayer::with_previous_keys and
encryption_from_bytes to state that the current master key and every previous
key must be at least 32 bytes. Replace the outdated “at least 16 bytes (32
recommended)” wording in encryption_from_bytes so both docs match the validation
enforced by the implementation.

In `@crates/cachekit/src/config.rs`:
- Around line 266-278: Update decode_master_key_hex to return Zeroizing<Vec<u8>>
and wrap the successfully decoded bytes before returning, while preserving its
existing validation and error behavior. Remove redundant Zeroizing::new wrapping
at all four callers, including the current- and previous-key validation paths,
so the returned wrapper owns the decoded key material for its full lifetime and
zeroizes it on drop.
- Around line 114-134: Update the CACHEKIT_PREVIOUS_MASTER_KEYS handling in the
configuration loading function to reject a configured non-empty previous-key
list when config.master_key is absent, before storing the keys, so encryption is
never silently disabled; preserve validate_previous_master_keys for
configurations with a current master key. Treat a blank or whitespace-only
environment value as unset and skip parsing it, while continuing to reject empty
entries within a non-empty comma-separated list.

In `@crates/cachekit/src/encryption.rs`:
- Around line 392-402: Add boundary-success tests for the three-key limit: in
crates/cachekit/src/encryption.rs:392-402, add a unit test using three distinct
32-byte keys with EncryptionLayer::with_previous_keys(K2, &refs, TEST_TENANT)
and assert success; in crates/cachekit/tests/config_tests.rs:209-219, add a
builder test passing three keys to previous_master_keys and assert
config.previous_master_keys.len() == 3.
- Around line 155-161: The decrypt flow in the keyring currently discards which
key succeeded, preventing operators from measuring previous-key usage. Update
decrypt and its callers or result type to expose the winning key position
(including the current key as index zero) and record a counter keyed by that
position, while preserving plaintext behavior. Also distinguish exhausted key
attempts from malformed ciphertext in the decryption error path.
- Around line 27-47: Update the documentation for EncryptionLayer to remove the
truncated or incomplete text and state the intended Keyring behavior clearly.
Keep the existing zeroization implementation unchanged, including Keyring’s
derived Zeroize and ZeroizeOnDrop behavior.

In `@crates/cachekit/tests/config_tests.rs`:
- Around line 316-320: Update the drift-guard comment above
previous_key_cap_matches_core_keyring_cap to state that MAX_PREVIOUS_MASTER_KEYS
is declared outside the encryption feature gate, while noting that the guard
runs in CI with encryption enabled.

---

Outside diff comments:
In `@crates/cachekit/src/encryption.rs`:
- Around line 85-98: In the validation block containing the previous_keys loop
and master_key_bytes check, move the current master-key length validation before
iterating over previous_keys so a short current key is reported first when both
are invalid. Preserve the existing CachekitError::Encryption variant and all
current error messages; do not change the previous-key validation behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 388ca305-c5d1-4aac-ad69-62c8be43645a

📥 Commits

Reviewing files that changed from the base of the PR and between 2aae7b5 and 737d77f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • README.md
  • crates/cachekit/Cargo.toml
  • crates/cachekit/src/client.rs
  • crates/cachekit/src/config.rs
  • crates/cachekit/src/encryption.rs
  • crates/cachekit/tests/config_tests.rs
  • crates/cachekit/tests/encryption_tests.rs

Comment thread crates/cachekit/src/client.rs
Comment thread crates/cachekit/src/config.rs
Comment thread crates/cachekit/src/config.rs
Comment thread crates/cachekit/src/encryption.rs
Comment thread crates/cachekit/src/encryption.rs
Comment thread crates/cachekit/src/encryption.rs
Comment thread crates/cachekit/tests/config_tests.rs Outdated
…class (LAB-686 panel)

Expert-panel findings on PR #63:

- decrypt: EncryptionError::KeyDerivation / KeyringIndexOutOfRange now map
  to CachekitError::Config instead of folding into ::Encryption — per the
  LAB-683 decision, a config bug must never masquerade as a decrypt
  failure that fail-open callers read as a miss.
- with_previous_keys: master-key length and tenant_id checks now return
  CachekitError::Config, matching the previous-key and Keyring checks in
  the same function (was: Encryption for master/tenant, Config for
  previous — inconsistent within one constructor).
- from_env: CACHEKIT_PREVIOUS_MASTER_KEYS without CACHEKIT_MASTER_KEY is
  now a load-time Config error instead of silently never wiring the
  previous keys (the botched-rotation-deploy case).
- intent_tests: short-master-key assertion updated to the Config class
  (the test's intent — validation before network I/O — unchanged).
@kodus-27b

This comment has been minimized.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

Crypto Expert-Panel Review (mandatory gate, high stakes)

Four-agent panel (bug-hunter-supreme, security-specialist, code-craftsman, catchphrase-agent) ran against this diff + the cachekit-core 0.5.0 Keyring source. Verdict: SHIP after fixes — applied in 97537b1.

Findings applied (97537b1)

  • [CRIT] Error-class collapse in decryptEncryptionError::KeyDerivation / KeyringIndexOutOfRange were folded into CachekitError::Encryption, contradicting the LAB-683 decision (config bugs must never masquerade as decrypt failures fail-open callers read as misses). Now mapped to CachekitError::Config.
  • [MAJ] Inconsistent error classes in with_previous_keys — short master / bad tenant returned Encryption while short previous key returned Config. All config-validation checks now return Config (intent test updated accordingly — its intent, validation-before-network-I/O, is unchanged).
  • [MAJ] Previous keys without a current key silently droppedCACHEKIT_PREVIOUS_MASTER_KEYS set with CACHEKIT_MASTER_KEY absent passed from_env and never wired encryption. Now a load-time Config error, with test.

Findings deferred (with reason)

  • [MAJ] HKDF re-derivation per decrypt attempt (every secure read pays a fresh HKDF; cached derived_key unused on the decrypt path). Both agents agreed the fix belongs in cachekit-core — LAB-683 forbids re-implementing/fast-pathing keyring sequencing in SDKs. Filed as LAB-1638 (Keyring::for_tenant derived-key cache).

Findings rejected (with reason)

  • Delete the config-level self-collision check, rely on Keyring::new — rejected: the ticket AC mandates a build/load-time error with an env-pointed message, and catchphrase-agent explicitly defended the fail-fast mirror; the check is byte-equality on the same operands as core, so drift risk is nil.
  • Embed keyring attempt count in decrypt error messages — rejected: message churn for marginal diagnostic value.

Panel positives

Forward-only invariant enforced at both load and build; SDK tightens core's 16-byte floor to 32; no key material in any Debug/error output; AAD built once per decrypt and identical across attempts (no oracle introduced); rotation e2e proves reads never rewrite stored ciphertext and hard cut-over fails closed; drift-guard pins the config cap to cachekit_core::MAX_DECRYPT_ONLY_KEYS.

Comment thread crates/cachekit/tests/config_tests.rs

@kodus-27b kodus-27b Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found critical issues please review the requested changes

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cachekit/tests/config_tests.rs`:
- Around line 330-333: Update the environment setup in the
CachekitConfig::from_env test to use an RAII guard for CACHEKIT_MASTER_KEY and
CACHEKIT_PREVIOUS_MASTER_KEYS. Preserve each variable’s original value and
restore it automatically on scope exit, including assertion failures, instead of
unconditionally removing them.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e98a16b6-2f82-4d73-9372-84f65c9c29be

📥 Commits

Reviewing files that changed from the base of the PR and between 737d77f and 97537b1.

📒 Files selected for processing (4)
  • crates/cachekit/src/config.rs
  • crates/cachekit/src/encryption.rs
  • crates/cachekit/tests/config_tests.rs
  • crates/cachekit/tests/intent_tests.rs

Comment thread crates/cachekit/tests/config_tests.rs Outdated
…, boundary tests (LAB-686)

CodeRabbit + Kody round on #63:

- decode_master_key_hex returns Zeroizing<Vec<u8>> so decoded key material
  is wiped on every path, including early-drop when a later validation
  step fails (CodeRabbit, security)
- A wholly blank CACHEKIT_PREVIOUS_MASTER_KEYS is treated as unset:
  blanking a variable is how shell profiles / Compose / k8s manifests
  retire it after a completed rotation; a blank entry inside a non-blank
  list is still rejected (CodeRabbit, correctness)
- Boundary-success tests for the cap: exactly three previous keys build
  (layer + config builder) — a >= regression would have passed the
  rejecting-side suite (CodeRabbit)
- EnvGuard RAII helper in config_tests: env tests restore pre-test
  variable values on drop, including on assertion failure (CodeRabbit)
- Doc fixes: encryption_from_bytes said 'at least 16 bytes' while the
  code rejects <32; with_previous doc states the 32-byte minimum;
  EncryptionLayer rotation doc line-wrap; drift-guard comment now says
  the cap const is declared outside the encryption feature gate
@kodus-27b

This comment has been minimized.

Comment thread crates/cachekit/src/encryption.rs

@kodus-27b kodus-27b Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found critical issues please review the requested changes

…lts test (LAB-686)

Expert-panel findings on the review-round commit:

- Convert the nine pre-existing env tests to EnvGuard: the old
  set/call/remove pattern skips cleanup when the call panics, poisoning
  every later #[serial] test — the exact class EnvGuard was added to kill
- config_from_env_defaults now clears CACHEKIT_PREVIOUS_MASTER_KEYS too;
  a shell-exported value would fail it with an unrelated
  requires-master-key error
- Comment why blank CACHEKIT_MASTER_KEY stays strict while blank
  previous-keys is tolerated: blank-as-unset on the master key would
  silently turn encryption off
- Drop a comment duplicated from the encryption.rs twin test
@kodus-27b

This comment has been minimized.

Comment thread crates/cachekit/tests/config_tests.rs

@kodus-27b kodus-27b Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found critical issues please review the requested changes

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@kody start-review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cachekit/src/config.rs`:
- Around line 112-149: The environment values read in the config loading path
must be wrapped in zeroize::Zeroizing<String> before parsing so copied secrets
are cleared. Update the relevant EnvGuard saved-value type to
Option<Zeroizing<String>> and restore saved variables using value.as_str();
apply the corresponding test fixture changes in
crates/cachekit/tests/config_tests.rs lines 199-230, while preserving existing
parsing and validation behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fc2386ef-e509-4f82-9d3e-16353b85595a

📥 Commits

Reviewing files that changed from the base of the PR and between 97537b1 and 7e2a98f.

📒 Files selected for processing (5)
  • README.md
  • crates/cachekit/src/client.rs
  • crates/cachekit/src/config.rs
  • crates/cachekit/src/encryption.rs
  • crates/cachekit/tests/config_tests.rs

Comment thread crates/cachekit/src/config.rs
kodus-27b[bot]
kodus-27b Bot previously approved these changes Aug 7, 2026
std::env::var hands back an owned String of the hex-encoded key material for
CACHEKIT_MASTER_KEY and CACHEKIT_PREVIOUS_MASTER_KEYS. The decoded bytes were
already Zeroizing, but the hex source copy was dropped without being wiped —
the same secret in a different encoding, equally recoverable from freed heap.

EnvGuard in the config tests had the same hole: it saves each variable's
pre-test shell value, which for those two vars is real operator key material.
@kodus-27b

This comment has been minimized.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 7, 2026
Comment thread crates/cachekit/src/config.rs
Panel note: the comment implied wrapping the env string closes the exposure. It
does not -- the process environ block holds the identical hex for the process
lifetime and is not wiped. Name that, so the next reader does not over-trust it.
@kodus-27b

kodus-27b Bot commented Aug 7, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

Zeroise finding addressed — 7a7d3f0 + 79bee95

config.rs:112,125std::env::var returns an owned String holding the hex-encoded key material for CACHEKIT_MASTER_KEY and CACHEKIT_PREVIOUS_MASTER_KEYS. The decoded bytes were already Zeroizing via decode_master_key_hex, but the hex source copy was dropped unwiped — the same secret in a different encoding. Both are now Zeroizing<String>, matching the api_key pattern already at config.rs:99.

config_tests.rsEnvGuard.saved is now Option<Zeroizing<String>>, restored via value.as_str(). It saves each variable's pre-test shell value, which for those two vars is real operator key material.

Scope correction in 79bee95: the comment originally implied this closed the exposure. It does not — the process environ block still holds the identical hex for the process lifetime and is not wiped here. This narrows post-lifetime recovery (core dumps, swap, heap reuse) only, and the comment now says so rather than letting the next reader over-trust it.

Verified: cargo test -p cachekit-rs --test config_tests 26/26 pass, cargo clippy -p cachekit-rs --all-targets -D warnings clean, cargo fmt clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline 7a7d3f0 79bee95
git show --format=fuller --find-renames --find-copies 7a7d3f0 -- \
  crates/cachekit/src/config.rs crates/cachekit/tests/config_tests.rs
git show --format=fuller --find-renames --find-copies 79bee95 -- \
  crates/cachekit/src/config.rs crates/cachekit/tests/config_tests.rs

ast-grep outline crates/cachekit/src/config.rs --items all --type function,struct,impl --match 'CachekitConfig|CachekitConfigBuilder|from_env|decode_master_key_hex'
ast-grep outline crates/cachekit/tests/config_tests.rs --items all --type function,struct,impl --match 'EnvGuard'

rg -n -C 8 'Zeroizing<String>|CACHEKIT_(MASTER_KEY|PREVIOUS_MASTER_KEYS)|decode_master_key_hex|struct EnvGuard|impl (Drop for )?EnvGuard' \
  crates/cachekit/src/config.rs crates/cachekit/tests/config_tests.rs

Length of output: 6961


@27Bslash6, reviewed the changes in 7a7d3f0 and 79bee95.

CACHEKIT_MASTER_KEY and CACHEKIT_PREVIOUS_MASTER_KEYS now wrap their owned std::env::var values in Zeroizing<String>. EnvGuard.saved also zeroizes saved key environment values after restoration. The scope comment correctly states that this does not wipe the process environment block.

I found no remaining issue with the zeroization finding.


✏️ Learnings added
Learnt from: 27Bslash6
URL: https://github.com/cachekit-io/cachekit-rs/pull/63

Timestamp: 2026-08-07T16:49:29.405Z
Learning: In `crates/cachekit/src/config.rs`, wrapping the `String` returned by `std::env::var` in `Zeroizing<String>` wipes the owned heap copy of `CACHEKIT_MASTER_KEY` or `CACHEKIT_PREVIOUS_MASTER_KEYS` on drop. It does not wipe the corresponding process environment block, which can retain the same key material for the process lifetime.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

Kody thread on config.rs:126 — rejected as a rule misfire

Rule 'Avoid unsafe type assertions' (f65a1f39) describes TypeScript as casts, which perform no runtime check. The flagged line is:

let val = Zeroizing::new(val);

That is a struct constructor taking ownership of a String, not a type assertion. Rust has no unchecked cast here — Zeroizing<T> is a Deref wrapper whose only behaviour is wiping the buffer on drop. There is nothing to type-guard.

Same class as rule d1910827 ('Don't use assert for data validation', a python -O rule) firing on Rust assert! in #[test] code earlier on this PR. Both rules appear to be language-scoped in intent but unscoped in configuration — worth restricting them to their intended languages, since they will keep re-firing on every Rust push.

Kody's review on this PR is APPROVED; this thread is non-blocking. CI is green across all 10 checks.

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.

1 participant