Skip to content

feat!: replace dead key-rotation machinery with keyring decrypt helper (LAB-683) - #67

Merged
27Bslash6 merged 3 commits into
mainfrom
lab-683-keyring-decrypt-helper
Aug 7, 2026
Merged

feat!: replace dead key-rotation machinery with keyring decrypt helper (LAB-683)#67
27Bslash6 merged 3 commits into
mainfrom
lab-683-keyring-decrypt-helper

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR replaces the previously unimplemented key-rotation machinery with a functional multi-key decrypt keyring that supports master-key rotation through a grace window. This is a breaking change (feat!).

What Changed

Removed dead code

  • Deleted the key_rotation.rs module, including KeyRotationState, RotationAwareHeader, and the EncryptionHeader type alias.
  • Removed the rotate_key() stub method on ZeroKnowledgeEncryptor, which had always returned a NotImplemented error.
  • Removed the associated NotImplemented error variant and related header roundtrip/algorithm tests.

Added new keyring functionality

  • Introduced a new keyring.rs module with a Keyring type that holds one current master key (used for both encryption and decryption) plus an ordered list of up to MAX_DECRYPT_ONLY_KEYS (3) decrypt-only keys retained during rotation.
  • Decryption attempts keys sequentially (current key first), advancing to the next key only on AES-GCM authentication failure. Structural ciphertext errors are terminal.
  • Added decrypt_at() for fingerprint-based key selection and encryption_fingerprints() for matching against per-tenant derived-key fingerprints.
  • All master-key material zeroizes on drop, including decrypt-only entries.

Enforced construction invariants

Three new error variants validate keyring configuration:

  • InvalidMasterKeyLength — every key must be at least 16 bytes.
  • KeyringCapExceeded — rejects (never truncates) more than 3 decrypt-only keys.
  • CurrentKeyInDecryptOnlyList — enforces the forward-only rotation rule (a key that once encrypted is never re-promoted).

Supporting updates

  • Updated FFI error mapping for the three new error variants.
  • Updated public re-exports in lib.rs and mod.rs to expose Keyring and MAX_DECRYPT_ONLY_KEYS.
  • Updated README.md architecture diagram and file listing to reflect the keyring-based rotation model.

Why

The prior key-rotation code was a non-functional stub. This change delivers a working rotation strategy where rotation state is treated as configuration rather than a state machine — the ciphertext format and AAD carry no key identity, so nothing on the wire changes. Values encrypted under a retiring key remain readable as long as that key stays in the decrypt-only list, enabling zero-downtime master-key rotation with a grace window.

Breaking Changes

  • Removed public APIs: KeyRotationState, RotationAwareHeader, EncryptionHeader, and ZeroKnowledgeEncryptor::rotate_key().
  • Removed the EncryptionError::NotImplemented variant.

Summary

This PR refines the keyring decryption helper by introducing distinct, semantically-precise error types that prevent configuration errors and caller bugs from being misinterpreted as cache misses under fail-open policies.

Key Changes

New distinct error variants

  • Added EncryptionError::KeyringIndexOutOfRange { index, count } to explicitly signal an out-of-range keyring entry index (a caller bug), replacing the previous generic DecryptionFailed string error.
  • Added EncryptionError::KeyDerivation (wrapping KeyDerivationError) to surface key-derivation failures (e.g. an invalid/empty tenant_id) as a configuration error rather than a decryption failure.

Prevention of fail-open misclassification

The core motivation is safety: previously, an out-of-range index or a bad tenant_id would surface as DecryptionFailed, which a fail-open SDK could mistake for a legitimate cache miss. Now these terminal errors are kept distinct from AuthenticationFailed/DecryptionFailed, so a misconfiguration cannot masquerade as a miss.

FFI error mapping

  • KeyringIndexOutOfRange maps to CachekitError::InvalidInput.
  • KeyDerivation delegates to the underlying KeyDerivationError conversion.

API surface reduction

  • Made Keyring::entry_count() private (bindings can derive the count from encryption_fingerprints().len()).
  • Removed the public decrypt_only_count() method.

Security hardening for logging

  • Replaced the derived Debug impl on TenantKeys with a manual one that redacts key material (CWE-215), printing only the tenant_id and encryption-key fingerprint.

Tests

  • Updated the out-of-range test to assert the new KeyringIndexOutOfRange variant.
  • Added test_bad_tenant_id_is_config_error_not_miss verifying an empty tenant_id yields KeyDerivation rather than AuthenticationFailed.
  • Removed the now-obsolete test_entry_counts test.

Breaking Change

Marked feat! — this removes the public entry_count() and decrypt_only_count() methods and changes the error variants returned by keyring decryption operations.


Based on the code changes provided, here's a description for this pull request:

Description

This PR updates the documentation for the decrypt_at method in the Keyring implementation, clarifying the distinction between two different error conditions that can occur during decryption.

What Changed

The doc comment for decrypt_at was revised to separate two previously conflated error cases:

  • Before: AuthenticationFailed was documented as covering both authentication failures and structural ciphertext errors (like InvalidCiphertext) from the underlying AES-GCM decrypt.
  • After: The two error conditions are now documented as distinct outcomes:
    • AuthenticationFailed — when the entry's key does not authenticate the ciphertext.
    • InvalidCiphertext — for malformed ciphertext.

Purpose

This change improves the accuracy of the API documentation by clearly distinguishing between an authentication failure (wrong key) and a structural/malformed ciphertext error, making the contract for callers of decrypt_at clearer and more precise.

Note on Context

The PR title (feat!: replace dead key-rotation machinery with keyring decrypt helper (LAB-683)) suggests a broader set of changes involving replacement of key-rotation machinery. However, the single diff provided only contains a documentation comment update to decrypt_at, so this description is limited to the code change shown.

Summary by CodeRabbit

  • New Features

    • Added support for master-key rotation using a current key and decrypt-only keys during a transition period.
    • Data can be decrypted across supported keys, with automatic retry after authentication failures.
    • Added key fingerprints to help identify derived encryption keys without exposing key material.
    • Added safeguards for invalid keys, duplicate keys, excessive keyring size and invalid key selections.
  • Documentation

    • Updated security architecture documentation to describe decrypt-only keyrings and the revised rotation approach.

…r (LAB-683)

Deletes the rotation dead code the LAB-275 audit flagged as a trust bug and
implements the cachekit-core half of the keyring decision
(protocol/decisions/key-rotation.md, merged via protocol#34):

- Remove ZeroKnowledgeEncryptor::rotate_key() NotImplemented stub and the
  EncryptionError::NotImplemented variant
- Delete src/encryption/key_rotation.rs whole (KeyRotationState,
  RotationAwareHeader) and the EncryptionHeader alias + re-exports
- Add encryption::keyring::Keyring: one current master key + at most 3
  decrypt-only keys; sequential decrypt current-first with identical AAD per
  attempt; per-entry HKDF-derived encryption-key fingerprints for
  fingerprint-based selection; cap and forward-only self-collision rejected at
  construction; all key material zeroized on drop

BREAKING CHANGE: public items EncryptionHeader, KeyRotationState,
RotationAwareHeader, and ZeroKnowledgeEncryptor::rotate_key() are removed
(EncryptionError::NotImplemented variant included). Use Keyring for
master-key rotation.
@kodus-27b

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The encryption module replaces rotation-aware state with a decrypt-only keyring. The keyring validates master keys, derives tenant keys, supports ordered decryption fallback, updates error mappings, and changes public exports and architecture documentation.

Changes

Master-key decrypt keyring

Layer / File(s) Summary
Keyring contracts and construction
src/encryption/core.rs, src/encryption/keyring.rs
The keyring defines capacity and validation errors. Construction validates key lengths, key count, and duplicate current keys. Key material is zeroised when the keyring is dropped.
Keyring decryption and validation
src/encryption/keyring.rs
The keyring derives tenant keys, generates fingerprints, supports indexed decryption, and tries the current key before decrypt-only keys. Tests cover fallback, authentication failures, AAD, bounds, fingerprints, and zeroisation.
Public API and error wiring
src/encryption/mod.rs, src/lib.rs, src/encryption/core.rs, src/ffi/error.rs, src/encryption/key_derivation.rs, README.md
The public API exports Keyring and MAX_DECRYPT_ONLY_KEYS, while rotation-state exports and rotate_key are removed. FFI mappings handle the new errors. TenantKeys debug output redacts key material. The architecture documentation names the keyring implementation.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Keyring
  participant ZeroKnowledgeEncryptor
  participant AESGCM
  Caller->>Keyring: decrypt ciphertext for tenant with AAD
  Keyring->>ZeroKnowledgeEncryptor: derive per-entry tenant key
  Keyring->>AESGCM: try current key
  AESGCM-->>Keyring: authentication failure
  Keyring->>AESGCM: try ordered decrypt-only key
  AESGCM-->>Keyring: plaintext or terminal error
  Keyring-->>Caller: return decryption result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 replacement of the obsolete key-rotation machinery with a keyring decrypt helper.
✨ 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-683-keyring-decrypt-helper

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

kodus-27b[bot]
kodus-27b Bot previously approved these changes Aug 5, 2026

@coderabbitai coderabbitai Bot 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.

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/mod.rs`:
- Around line 15-20: Update Keyring::new to accept only master keys with exactly
32 bytes, rejecting both shorter and longer inputs; update
InvalidMasterKeyLength to represent the fixed 32-byte requirement. Apply the
public API change consistently at src/encryption/mod.rs lines 15-20 and
src/lib.rs lines 83-84, preserving both re-export paths.
🪄 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: 0cbc0994-1e12-4eec-ae44-a8c30b5d7565

📥 Commits

Reviewing files that changed from the base of the PR and between daf3459 and bd19d3d.

📒 Files selected for processing (7)
  • README.md
  • src/encryption/core.rs
  • src/encryption/key_rotation.rs
  • src/encryption/keyring.rs
  • src/encryption/mod.rs
  • src/ffi/error.rs
  • src/lib.rs
💤 Files with no reviewable changes (1)
  • src/encryption/key_rotation.rs

Comment thread src/encryption/mod.rs
…l findings)

Panel findings applied (LAB-683 crypto/protocol review gate):

- Key-derivation failure and decrypt_at out-of-range index no longer fold
  into DecryptionFailed (same FFI code as AuthenticationFailed): new
  KeyDerivation(#[from] KeyDerivationError) and KeyringIndexOutOfRange
  variants map to input/config errors so fail-open SDK policies cannot
  mistake a bad tenant_id or a binding indexing bug for a cache miss
  (LAB-241 signal-ambiguity class)
- TenantKeys: manual Debug impl redacting key material (CWE-215); prints
  tenant_id + encryption fingerprint only
- Cut unused public surface before the SDK children copy the API:
  decrypt_only_count() deleted, entry_count() now private (bindings use
  encryption_fingerprints().len())
- decrypt()/decrypt_at() # Errors docs now list every terminal variant
@kodus-27b

This comment has been minimized.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

Expert-panel review record (crypto/protocol gate, LAB-683)

Panel ran at critical-stakes (foundational crypto library, all three SDKs consume this API next): bug-hunter-supreme, security-specialist, code-craftsman, catchphrase-agent, all reviewing the full diff against the normative spec (protocol/spec/encryption.md § Key Rotation (Keyring)) and the protocol #34 decision record.

Verdict: SHIP (after fixes in 74f85d2). No CRIT findings. Spec conformance verified clean by two agents independently: cap-3 reject-never-truncate, forward-only self-collision rejection, fingerprint over the HKDF-derived per-tenant key (byte-verified against derive_tenant_keys), identical AAD across sequential attempts, exhaustion = plain AuthenticationFailed (no error oracle, no new failure mode), zeroize coverage proven for decrypt-only entries, wire format/AAD v0x03 untouched.

Findings applied (commit 74f85d2)

  1. [MAJ, converged — all 3 specialists] Key-derivation failures and decrypt_at out-of-range indexes folded into DecryptionFailed, which the FFI collapses into the same code as AuthenticationFailed — a fail-open SDK caller couldn't distinguish "legit miss" from "config/binding bug" (exact LAB-241 signal-ambiguity class). → New EncryptionError::KeyDerivation and KeyringIndexOutOfRange variants, FFI-mapped to input errors; DecryptionFailed/AuthenticationFailed now strictly crypto. Regression test added (test_bad_tenant_id_is_config_error_not_miss).
  2. [MAJ, security] TenantKeys derived Debug printed all three derived keys (CWE-215). → Manual Debug impl, keys redacted, fingerprint only.
  3. [Cut list, catchphrase + craftsman] decrypt_only_count() had zero planned consumers (LAB-388 orphaned-API pattern); entry_count() needed only internally. → Deleted / made private before LAB-684/685/686 copy the API surface.
  4. [MIN, craftsman] decrypt()/decrypt_at() # Errors docs missed terminal variants. → Docs now enumerate them.

Findings rejected

None — every surviving finding was applied.

All gates re-run green after fixes: cargo test --all-features (11 suites + doc-tests), clippy --all-features -D warnings, fmt --check, wasm32 cargo check.

kodus-27b[bot]
kodus-27b Bot previously approved these changes Aug 5, 2026

@coderabbitai coderabbitai Bot 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.

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 171-174: Update the decrypt_at error documentation to list
EncryptionError::InvalidCiphertext as a separate terminal error for malformed
ciphertext, while limiting EncryptionError::AuthenticationFailed to
authentication failures; preserve the existing behavior in decrypt_aes_gcm and
the unchanged error propagation in decrypt_at.
🪄 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: 3fe5788b-def6-4f68-9f12-e505a34e9856

📥 Commits

Reviewing files that changed from the base of the PR and between bd19d3d and 74f85d2.

📒 Files selected for processing (4)
  • src/encryption/core.rs
  • src/encryption/key_derivation.rs
  • src/encryption/keyring.rs
  • src/ffi/error.rs

Comment thread src/encryption/keyring.rs Outdated
decrypt_at passes EncryptionError::InvalidCiphertext through unchanged
from the underlying AES-GCM decrypt; documenting it as a parenthetical
under AuthenticationFailed misstated which variant callers receive.
@kodus-27b

kodus-27b Bot commented Aug 5, 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

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

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.

@27Bslash6
27Bslash6 merged commit 327f993 into main Aug 7, 2026
33 checks passed
@27Bslash6
27Bslash6 deleted the lab-683-keyring-decrypt-helper branch August 7, 2026 04:34
27Bslash6 added a commit that referenced this pull request Aug 7, 2026
…69)

Without bump-minor-pre-major, the feat! on #67 made release-please
propose 1.0.0. The LAB-683 release AC ratifies the next 0.x minor
(0.5.0) — we are not promising 1.0 API stability yet.
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