feat: expose winning-key index via decrypt_indexed for rotation drain observability (LAB-1645) - #73
Conversation
… observability (LAB-1645) During a rotation grace window an operator has no signal for when it is safe to drop a retiring master key: Keyring::decrypt and TenantKeyring::decrypt collapse the result to plaintext-or-error, discarding which entry satisfied the read, so "previous-key hit rate has reached zero" is unobservable and dropping a key risks a hard cut-over. Add decrypt_indexed to both Keyring and TenantKeyring, returning (plaintext, winning index) with 0 = current key. The sequencing loop moves into decrypt_indexed and decrypt delegates to it, so attempt semantics (current-first, identical AAD, only AuthenticationFailed advances, structural/config errors terminal, exhaustion = plain AuthenticationFailed) live in exactly one place per type and cannot drift between the two surfaces. Existing signatures unchanged; the new surface carries index only — no key material.
…45 panel) Expert-panel findings: the feature's whole point (non-zero index on a previous-key read) had no runnable example in a crate where doc-tests are the executable docs — add one asserting index == 1 for a retiring- key read. TenantKeyring::decrypt_indexed restated the drain narrative verbatim; cut to the Keyring cross-ref plus this type's genuine deltas (no HKDF, no KeyDerivation class) so duplicated prose cannot drift.
This comment has been minimized.
This comment has been minimized.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 42 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
WalkthroughThe change adds indexed decryption to ChangesKeyring decryption
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 `@src/encryption/keyring.rs`:
- Around line 254-271: Update the Keyring doctest to import zeroize::Zeroize,
declare k1, k2, and tenant_key as mutable, and call zeroize() on each after its
final use before scope exit. Preserve the existing encryption, key rotation, and
decrypt_indexed assertions.
🪄 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: d566c4b2-9499-4d87-8b85-4c54cf1e5304
📒 Files selected for processing (1)
src/encryption/keyring.rs
…decrypt_indexed doctest Keyring::new copies the master keys, so ZeroizeOnDrop clears only the keyring's copies — the example now wipes the caller-owned buffers after their last use, modelling the full hygiene a crypto-crate example should teach. CodeRabbit-Resolved: src/encryption/keyring.rs:271:Zeroise the doctest key buffers
Kody Review CompleteGreat news! 🎉 Keep up the excellent work! 🚀 Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
|
@coderabbitai review |
|
Closes LAB-1645.
Problem
During a rotation grace window an operator has no signal for when it is safe to drop a retiring master key:
Keyring::decryptandTenantKeyring::decryptcollapse the result to plaintext-or-error, discarding which entry satisfied the read. "Previous-key hit rate has reached zero" is unobservable, so dropping a key is guesswork — and guessing wrong is a hard cut-over (every pre-rotation entry becomes an error). Raised by CodeRabbit on cachekit-io/cachekit-rs#63 and deferred to core because SDKs must not re-implement keyring attempt logic (LAB-683 decision).Change
Additive
decrypt_indexedon bothKeyringandTenantKeyring, returning(Vec<u8>, usize)— the plaintext plus the winning keyring entry index (0 = current key, 1.. = decrypt-only keys in list order). SDKs count non-zero-index reads; when that rate reaches zero the retiring key is drained and safe to drop.The sequencing loop moved into
decrypt_indexedand the existingdecrypton each type became a one-line delegate, so attempt semantics (current-first, identical AAD, onlyAuthenticationFailedadvances, structural/config errors terminal, exhaustion = plainAuthenticationFailed) live in exactly one place per type and cannot drift between the two surfaces. Existing signatures unchanged — plain additivefeat:(0.x minor). The new surface carries the index only: no key material, no fingerprint; ZeroizeOnDrop discipline untouched.Tests
test_decrypt_indexed_reports_winning_entry— index 0 on a current-key hit, 1 on a previous-key hit, plaintext identical todecrypt.test_decrypt_indexed_exhaustion_and_terminal_errors_match_decrypt— exhaustion stays plainAuthenticationFailed;InvalidCiphertext/KeyDerivationstay terminal (LAB-683 no-collapse rule).test_tenant_keyring_decrypt_indexed_matches_unbound— same contract on the tenant-bound (SDK steady-state) path.Keyring::decrypt_indexedasserting the drain signal itself (index == 1for a retiring-key read).cargo fmt --check,cargo clippy --all-targets --all-features -D warnings,cargo test --all-features(99 unit + 4 doc-tests + integration targets) all green locally.Review gate
Expert panel (critical-stakes, crypto surface) run pre-PR: bug-hunter, security-specialist, and catchphrase returned no findings; code-craftsman's two findings (runnable doc-test for the drain signal; dedupe the
TenantKeyringrustdoc to a cross-ref plus genuine deltas) are applied in the second commit.Docs
Rustdoc on the new surface states the operator workflow (previous-key hit rate → zero ⇒ safe to drop) with an executable example. No other doc surfaces change: SDK exposure is explicitly out of scope (follow-up children per SDK), no wire/spec behaviour changes, README mentions the keyring only at architecture-diagram level, CHANGELOG is release-please-managed.
Summary by CodeRabbit
New Features
Bug Fixes
Tests