Time-Based Keyset Selection: Achieving Safe Key Rotation
Decryption in a distributed system is often a balancing act between security and availability. For a long time, the simplest approach was to use the “latest key” to decrypt content. However, as systems scale and security requirements evolve, rotating those keys becomes a necessity.
The challenge? If you rotate a key, how do you ensure that content encrypted with the old key remains accessible without performing a massive, expensive re-encryption of all historical data?
We recently solved this by implementing Time-Based Keyset Selection and Event-Versioned Key Derivation. Here is a deep dive into how it works.
The Core Idea: Versioning by Time
The fundamental shift in our architecture is that we no longer treat the “latest key” as the source of truth for decryption. Instead, we use event-versioned key derivation.
When a user attempts to unlock a piece of content, the system follows a deterministic path:
- Identify the canonical event (the identity of the content).
- Read the event’s
created_attimestamp. - Fetch/Derive the key specific to that exact point in time.
- Decrypt using that specific version.
Because old events retain their original timestamps, they continue to resolve to their original keys, even after the system’s “active” key has rotated.
Server-Side: The Keyschedule
On the server, we don’t store explicit version numbers on every event. Instead, we use an implicit versioning system driven by a Keyset Schedule. This schedule consists of:
keyset_idactive_from(Unix timestamp)key_bindingsalt_binding
For any given timestamp $t$, the server selects the keyset with the largest active_from such that active_from <= t.
The Decryption Path
When a client requests a key via /request-key, the server:
- Resolves the canonical event for the provided identifier.
- Reads the
created_attimestamp. - Selects the corresponding keyset from the schedule.
- Derives the content key and returns it.
This ensures that the server always provides the correct key for the content’s “era,” regardless of how many rotations have happened since.
Client-Side: Version-Aware Caching
On the client, simply caching keys by their ID (UUID) is no longer sufficient. If a key rotates, a cached key for a specific ID might be “stale” or “too new” depending on which version of the event the user is looking at.
Our keyStore.ts now stores advanced metadata for every cached key:
uuidkeynaddr(The canonical event identity)eventCreatedAtsavedAt
Intelligent Reuse Logic
Before using a cached key, the client performs a strict check. A key is reused only if both the naddr and the eventCreatedAt of the content being unlocked match the stored record. If there is a mismatch, the client ignores the cache and fetches a fresh key from the server.
This logic is implemented across our core stores, including useUnlockStore.ts, ensuring that bulk unlocks and remote data synchronization remain version-aware and collision-free.
A Real-World Scenario: The Rotation Timeline
To see this in action, let’s look at a typical rotation timeline:
- T1: You publish Event A. The system uses the Current Keyset.
- R: A Server Rotation occurs. A new keyset becomes active.
- T2: You publish Event B. The system uses the New Keyset.
When Unlocking:
- Unlocking Event A: The worker sees
created_at = T1. SinceT1 < R, it uses the old keyset. Decryption succeeds. - Unlocking Event B: The worker sees
created_at = T2. SinceT2 > R, it uses the new keyset. Decryption succeeds.
Old content remains safe; new content uses rotated material.
Summary
By moving to a time-based keyset selection model, we’ve decoupled key rotation from content availability. Rotation is no longer a “breaking change”—it’s a routine operation that enhances security while preserving the integrity of our historical data.
Stay tuned for more updates as we continue to harden our cryptographic infrastructure!