Support zero-downtime rotation of shared private-state encryption keys #1237

Closed
opened 2026-08-21 21:02:52 +00:00 by timmy · 0 comments
Owner

Problem

All synchronized private-state stores use one unversioned STACKCHAIN_PRIVATE_STATE_ENCRYPTION_KEY. Replacing it makes existing Today/Week/Later plans, snapshots, saved searches, security events, and idempotency receipts unreadable, so routine or emergency rotation cannot be performed without downtime or state loss. Draft encryption already has a keyring, but this shared cipher does not.

Scope

  • Add a bounded keyring (STACKCHAIN_PRIVATE_STATE_ENCRYPTION_KEYS) and active key ID (STACKCHAIN_PRIVATE_STATE_ACTIVE_KEY_ID) while preserving the legacy single-key setting.
  • Write versioned key-ID envelopes and read plaintext, legacy v1, and current envelopes without weakening existing AAD bindings.
  • Provide a restart-safe, content-free rotation command for all shared private-state SQLite stores.
  • Document rollout/retirement and ship the command in deterministic release bundles.

Acceptance

  • Legacy ciphertext remains readable during rollout.
  • New writes use the active key ID.
  • Rotation rewraps legacy/plaintext/inactive-key rows without changing logical metadata and can be rerun safely.
  • Unknown keys, tampering, malformed config, and cross-record substitution fail closed.
  • Rotation output contains store/table counts only, never private content.
  • Focused and full suites pass; release bundle includes the command.

No duplicate exists among 620 historical issue titles, 613 PR titles, recent 200 commits, or releases through v0.1.0-rc.1488. The nearest prior work (#1100/#1101) covers only the independent Draft store.

## Problem All synchronized private-state stores use one unversioned `STACKCHAIN_PRIVATE_STATE_ENCRYPTION_KEY`. Replacing it makes existing Today/Week/Later plans, snapshots, saved searches, security events, and idempotency receipts unreadable, so routine or emergency rotation cannot be performed without downtime or state loss. Draft encryption already has a keyring, but this shared cipher does not. ## Scope - Add a bounded keyring (`STACKCHAIN_PRIVATE_STATE_ENCRYPTION_KEYS`) and active key ID (`STACKCHAIN_PRIVATE_STATE_ACTIVE_KEY_ID`) while preserving the legacy single-key setting. - Write versioned key-ID envelopes and read plaintext, legacy v1, and current envelopes without weakening existing AAD bindings. - Provide a restart-safe, content-free rotation command for all shared private-state SQLite stores. - Document rollout/retirement and ship the command in deterministic release bundles. ## Acceptance - Legacy ciphertext remains readable during rollout. - New writes use the active key ID. - Rotation rewraps legacy/plaintext/inactive-key rows without changing logical metadata and can be rerun safely. - Unknown keys, tampering, malformed config, and cross-record substitution fail closed. - Rotation output contains store/table counts only, never private content. - Focused and full suites pass; release bundle includes the command. No duplicate exists among 620 historical issue titles, 613 PR titles, recent 200 commits, or releases through v0.1.0-rc.1488. The nearest prior work (#1100/#1101) covers only the independent Draft store.
timmy self-assigned this 2026-08-21 21:02:53 +00:00
timmy closed this issue 2026-08-21 21:37:26 +00:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stackchain/stackchain-dashboard#1237
No description provided.