Skip to content

feat: M12 Carousel — key rotation without re-encrypting data (v5.2.0)#13

Merged
flyingrobots merged 6 commits intomainfrom
feat/m12-carousel
Feb 28, 2026
Merged

feat: M12 Carousel — key rotation without re-encrypting data (v5.2.0)#13
flyingrobots merged 6 commits intomainfrom
feat/m12-carousel

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Feb 28, 2026

Summary

  • Key rotation without re-encryptionrotateKey() re-wraps the DEK with a new KEK, leaving data blobs untouched. Enables key compromise response without re-storing assets.
  • keyVersion tracking — manifest-level and per-recipient counters for audit compliance.
  • rotateVaultPassphrase() — rotates the vault-level encryption passphrase across all envelope-encrypted entries in a single atomic commit with retry logic for concurrent updates.
  • CLI commandsgit cas rotate (single asset) and git cas vault rotate (vault-wide passphrase).
  • Polish — rotation helpers use native #private methods, CLI docs added to API.md, error code table updated, flapping test stabilized via reduced test-only KDF iterations.

Test plan

  • 784 unit tests pass on Node, Bun, and Deno
  • ESLint clean (0 errors)
  • Rotation integration test completes in ~2.7s (was 15s timeout)
  • No key material logged in error paths (security audit)
  • Retry logic refreshes vault state on each iteration (no stale OIDs)

Summary by CodeRabbit

Release Notes v5.2.1

  • New Features

    • Key rotation for envelope-encrypted content without re-encrypting data blobs
    • Vault passphrase rotation to atomically update encryption across all entries
    • New CLI commands: rotate and vault rotate for key and vault operations
    • Key version tracking to monitor encryption key lifecycles
  • Documentation

    • Expanded API, security, and user guides with comprehensive key rotation details
  • Version Update

    • Bumped to v5.2.1

Add rotateKey() on CasService for re-wrapping the DEK with a new KEK,
leaving data blobs untouched. Add rotateVaultPassphrase() on the facade
for atomic vault-wide passphrase rotation. keyVersion tracking at both
manifest-level and per-recipient level enables audit compliance.

New CLI commands: `git cas rotate` and `git cas vault rotate`.
New error code: ROTATION_NOT_SUPPORTED.
27 new unit tests (784 total). All three runtimes pass.
- Convert rotation helpers to #private methods in CasService
- Add CLI rotation command reference to docs/API.md
- Fix error code table to include rotateVaultPassphrase() contexts
- Reduce test-only KDF iterations to prevent CI flapping
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Warning

Rate limit exceeded

@flyingrobots has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 28 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between dd269b2 and 5f8f7ef.

📒 Files selected for processing (3)
  • GUIDE.md
  • test/integration/vault-cli.test.js
  • test/unit/facade/ContentAddressableStore.rotation.test.js
📝 Walkthrough

Walkthrough

Introduces key rotation capabilities for envelope-encrypted content. Adds rotateKey() and rotateVaultPassphrase() public APIs to ContentAddressableStore/CasService, with corresponding CLI commands (rotate, vault rotate). Extends Manifest schema with keyVersion tracking on recipients and encryption entries. Includes comprehensive test coverage, documentation updates, and version bump to 5.2.1.

Changes

Cohort / File(s) Summary
Version & Package Management
package.json, jsr.json, bin/git-cas.js
Version bumped from 5.1.0 to 5.2.1; CLI now includes two new rotation commands: vault rotate for vault passphrase rotation and rotate for key rotation.
Documentation & Changelog
CHANGELOG.md, README.md, GUIDE.md, SECURITY.md, docs/API.md, ROADMAP.md, COMPLETED_TASKS.md
Added comprehensive documentation for key rotation feature; updated API reference with rotateKey/rotateVaultPassphrase signatures and error codes; marked Carousel milestone (v5.2.0) as completed; SECURITY.md reframed from "No Key Rotation" to "Key Rotation (v5.2.0+)".
Error Handling
bin/actions.js
Added ROTATION_NOT_SUPPORTED hint for error messaging when key rotation is attempted on non-envelope-encrypted content.
Domain Schemas & Models
src/domain/schemas/ManifestSchema.js, src/domain/schemas/ManifestSchema.d.ts, src/domain/value-objects/Manifest.d.ts
Extended RecipientSchema and EncryptionSchema with optional keyVersion field (integer, min 0) to track key rotation iterations.
Core Service Implementation
src/domain/services/CasService.js, src/domain/services/CasService.d.ts
Added public method rotateKey({ manifest, oldKey, newKey, label }) with three private helpers: #findRecipientByLabel, #findRecipientByKey, #buildRotatedManifest; validates envelope presence, handles DEK unwrap/re-wrap, and increments keyVersion.
Public API & Facade
index.js, index.d.ts
Added ContentAddressableStore.rotateKey() and rotateVaultPassphrase() public methods; rotateVaultPassphrase orchestrates KEK derivation, per-entry envelope rotation, metadata updates, and commits with retry logic for transient VAULT_CONFLICT errors.
Integration Tests
test/integration/vault-cli.test.js
Tests vault init, store with recipient, key rotation via CLI, and restore with new key to validate rotation workflow and backward compatibility.
Unit Tests for Schemas
test/unit/domain/schemas/ManifestSchema.keyVersion.test.js
Validates keyVersion acceptance (0 and positive integers), rejection of negative/non-integer values, omission handling, and preservation across RecipientSchema, EncryptionSchema, and ManifestSchema.
Unit Tests for CasService
test/unit/domain/services/CasService.rotateKey.test.js
Covers golden path (rotate and restore), keyVersion incrementing, zero readBlob calls during rotation, label-based selective updates, auto-detection, error handling (NO_MATCHING_RECIPIENT, DEK_UNWRAP_FAILED, etc.), legacy manifest rejection, and sequential rotation sequences.
Unit Tests for ContentAddressableStore
test/unit/facade/ContentAddressableStore.rotation.test.js
Tests rotateVaultPassphrase across multiple envelopes, mixed envelope/non-envelope entries, wrong passphrase errors, unencrypted vault rejection, KDF algorithm override, salt updates, and restoration integrity.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CAS as CAS Service
    participant Crypto
    participant Git as Git Storage
    participant KDF

    Client->>CAS: rotateKey(manifest, oldKey, newKey, label)
    
    CAS->>CAS: Validate envelope encryption present
    CAS->>CAS: Find recipient by label or oldKey
    CAS->>Crypto: Unwrap DEK with oldKey
    Crypto-->>CAS: DEK
    
    CAS->>Crypto: Wrap DEK with newKey
    Crypto-->>CAS: Wrapped DEK (new)
    
    CAS->>CAS: Build rotated manifest<br/>(keyVersion++)
    CAS-->>Client: Updated Manifest
Loading
sequenceDiagram
    participant Client
    participant CAS as CAS Service
    participant Vault
    participant KDF
    participant Crypto
    participant Git as Git Storage

    Client->>CAS: rotateVaultPassphrase(oldPass, newPass)
    
    CAS->>Vault: Read vault metadata
    Vault-->>CAS: Metadata + entries
    
    CAS->>KDF: Derive old KEK from oldPass
    KDF-->>CAS: Old KEK
    
    CAS->>KDF: Derive new KEK from newPass
    KDF-->>CAS: New KEK
    
    loop For each envelope entry
        CAS->>Crypto: Unwrap DEK with old KEK
        Crypto-->>CAS: DEK
        CAS->>Crypto: Wrap DEK with new KEK
        Crypto-->>CAS: Wrapped DEK (new)
    end
    
    CAS->>Vault: Update metadata (new salt)
    CAS->>Git: Commit rotated state<br/>(with retry on conflict)
    Git-->>CAS: commitOid
    
    CAS-->>Client: {commitOid, rotatedSlugs, skippedSlugs}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Keys spinning 'round the vault so deep,
No data lost while secrets sleep,
Recipients dance with versions new,
Wrapping, unwrapping, through and through!
Fresh passphrases in the warren's keep. 🔐

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature: M12 Carousel milestone delivering key rotation without re-encrypting data in v5.2.0, which aligns with the comprehensive implementation across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/m12-carousel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Patch version for private method refactor, doc fixes, and test stability.
…ap polish

CR-1: algorithm flag was silently ignored — kdf.algorithm always overwrote
      kdfOptions.algorithm. Now uses kdfOptions?.algorithm || kdf.algorithm.
CR-2: CHANGELOG 5.2.1 section order → Added/Changed/Fixed per spec.
CR-3: Add VAULT_CONFLICT to rotateVaultPassphrase Throws in API.md.
CR-4: Move Task 12.1–12.4 from ROADMAP.md to COMPLETED_TASKS.md.
CR-5: Add @throws NO_MATCHING_RECIPIENT JSDoc to CasService.rotateKey().
CR-6: Split mixed CLI flag table into git-cas-rotate / vault-rotate tables.
createRepo() creates bare repos without user.name/user.email, causing
"Author identity unknown" on CI runners that lack global git config.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
test/integration/vault-cli.test.js (1)

240-250: Consider adding a negative test to verify old key rejection after rotation.

The current tests verify that the new key works after rotation, but don't verify that the old key is rejected. This would provide stronger confidence in the rotation behavior.

🧪 Suggested additional test
it('restore with old key fails after rotation', () => {
  const outDir = mkdtempSync(path.join(os.tmpdir(), 'cas-cli-rotate-fail-'));
  const outPath = path.join(outDir, 'restored.bin');
  expect(() => {
    cli(
      `restore --slug rotate/asset --out ${outPath} --key-file ${oldKeyFile}`,
      rotateRepoDir,
    );
  }).toThrow(); // or check for specific error code
  rmSync(outDir, { recursive: true, force: true });
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/vault-cli.test.js` around lines 240 - 250, Add a new
negative integration test that asserts the old key is rejected after rotation:
create a temp directory (using mkdtempSync), build outPath, call cli with the
same restore command but passing oldKeyFile and rotateRepoDir, and wrap that
call in expect(() => cli(...)).toThrow() (or assert a specific error), then
clean up the temp directory with rmSync; place this test alongside the existing
"restore with new key succeeds after rotation" test and reference the same
rotateOriginal/outPath setup so it validates failure with oldKeyFile.
GUIDE.md (1)

10-28: Consider adding section 11b to the Table of Contents.

The new "Multi-Recipient Encryption & Key Rotation" section (11b) is not listed in the Table of Contents. Users browsing the TOC won't discover this feature.

📝 Proposed addition
 22. [Passphrase Encryption (KDF)](`#11-passphrase-encryption-kdf`)
-23. [Merkle Manifests](`#12-merkle-manifests`)
+23. [Multi-Recipient Encryption & Key Rotation](`#11b-multi-recipient-encryption--key-rotation`)
+24. [Merkle Manifests](`#12-merkle-manifests`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@GUIDE.md` around lines 10 - 28, Add the missing TOC entry for the new
"Multi-Recipient Encryption & Key Rotation" section (labelled as 11b) by
inserting a linked item between the existing "11. [Passphrase Encryption
(KDF)](`#11-passphrase-encryption-kdf`)" and "12. [Merkle
Manifests](`#12-merkle-manifests`)" entries; use the heading text "11b.
[Multi-Recipient Encryption & Key
Rotation](`#11b-multi-recipient-encryption--key-rotation`)" so the link matches
the section anchor.
test/unit/facade/ContentAddressableStore.rotation.test.js (1)

100-132: Consider using low KDF iterations consistently across all tests.

The PR description mentions "Stabilizes a flapping test by reducing test-only KDF iterations." Line 66 uses { iterations: 1 }, but lines 103, 136, 145, 165, and 183 use default iterations which are much higher (100,000). This inconsistency could cause test slowness or flakiness.

♻️ Proposed fix for consistent KDF iterations
   it('mixed: 2 envelope + 1 non-envelope → 2 rotated, 1 skipped', async () => {
     const oldPass = 'old-pass';
     const newPass = 'new-pass';
-    await cas.initVault({ passphrase: oldPass });
+    await cas.initVault({ passphrase: oldPass, kdfOptions: { iterations: 1 } });

Apply similar changes to other test cases at lines 136, 145, 165, and 183.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/facade/ContentAddressableStore.rotation.test.js` around lines 100 -
132, The test uses default (high) KDF iterations for the vault while elsewhere
tests set iterations:1; fix by initializing the test vault with low iterations
so deriveKey uses iterations:1 consistently: call cas.initVault with an explicit
KDF options object (e.g., cas.initVault({ passphrase: oldPass, kdf: {
iterations: 1 } })) or otherwise ensure metadata.encryption.kdf.iterations is
set to 1 before calling cas.deriveKey/getVaultMetadata; apply the same change to
the other tests that call cas.initVault / cas.deriveKey so all tests use
iterations:1.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@GUIDE.md`:
- Around line 10-28: Add the missing TOC entry for the new "Multi-Recipient
Encryption & Key Rotation" section (labelled as 11b) by inserting a linked item
between the existing "11. [Passphrase Encryption
(KDF)](`#11-passphrase-encryption-kdf`)" and "12. [Merkle
Manifests](`#12-merkle-manifests`)" entries; use the heading text "11b.
[Multi-Recipient Encryption & Key
Rotation](`#11b-multi-recipient-encryption--key-rotation`)" so the link matches
the section anchor.

In `@test/integration/vault-cli.test.js`:
- Around line 240-250: Add a new negative integration test that asserts the old
key is rejected after rotation: create a temp directory (using mkdtempSync),
build outPath, call cli with the same restore command but passing oldKeyFile and
rotateRepoDir, and wrap that call in expect(() => cli(...)).toThrow() (or assert
a specific error), then clean up the temp directory with rmSync; place this test
alongside the existing "restore with new key succeeds after rotation" test and
reference the same rotateOriginal/outPath setup so it validates failure with
oldKeyFile.

In `@test/unit/facade/ContentAddressableStore.rotation.test.js`:
- Around line 100-132: The test uses default (high) KDF iterations for the vault
while elsewhere tests set iterations:1; fix by initializing the test vault with
low iterations so deriveKey uses iterations:1 consistently: call cas.initVault
with an explicit KDF options object (e.g., cas.initVault({ passphrase: oldPass,
kdf: { iterations: 1 } })) or otherwise ensure
metadata.encryption.kdf.iterations is set to 1 before calling
cas.deriveKey/getVaultMetadata; apply the same change to the other tests that
call cas.initVault / cas.deriveKey so all tests use iterations:1.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f4ab3a and dd269b2.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • COMPLETED_TASKS.md
  • GUIDE.md
  • README.md
  • ROADMAP.md
  • bin/actions.js
  • bin/git-cas.js
  • docs/API.md
  • docs/SECURITY.md
  • index.d.ts
  • index.js
  • jsr.json
  • package.json
  • src/domain/schemas/ManifestSchema.d.ts
  • src/domain/schemas/ManifestSchema.js
  • src/domain/services/CasService.d.ts
  • src/domain/services/CasService.js
  • src/domain/value-objects/Manifest.d.ts
  • test/integration/vault-cli.test.js
  • test/unit/domain/schemas/ManifestSchema.keyVersion.test.js
  • test/unit/domain/services/CasService.rotateKey.test.js
  • test/unit/facade/ContentAddressableStore.rotation.test.js

- Add missing 11b TOC entry in GUIDE.md for Multi-Recipient section
- Add negative integration test: old key rejected after rotation
- Use iterations:1 consistently in rotation unit tests for speed
@flyingrobots
Copy link
Member Author

All 3 nitpicks addressed in 5f8f7ef:

  1. GUIDE.md TOC — Added missing 11b entry for Multi-Recipient Encryption & Key Rotation.
  2. Old key rejection test — Added negative integration test verifying old key fails after rotation.
  3. KDF iterations consistency — All rotation unit tests now use iterations: 1 for speed.

✅ Addressed in commits af7f351, 5f8f7ef

@flyingrobots
Copy link
Member Author

@coderabbitai review please

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

@flyingrobots: I'll review the changes now. Thank you for addressing those nitpicks—the TOC addition, old key rejection test, and KDF iterations consistency all look like solid improvements. Let me take a comprehensive look at the key rotation implementation and documentation.

✅ Actions performed

Review triggered.

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.

@flyingrobots flyingrobots merged commit e2fbf76 into main Feb 28, 2026
6 checks passed
@flyingrobots flyingrobots deleted the feat/m12-carousel branch February 28, 2026 17:19
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