Key rotation
Resumable root-key rotation contracts, record-key rewrapping, atomic cutover requirements, checkpoints, and current implementation status.
Root-key rotation is a coordinated client and server operation. Record plaintext remains a browser concern. The application backend persists only progress metadata, replacement device envelopes, and the final vault key version.
If only the recovery password or file needs to change, use standalone Recovery Kit replacement instead. It preserves the root key, record wrappers, and device envelopes.
Transaction shape
- Begin a single-step rotation from key version
nton + 1using the current vault revision. - Generate the new root key locally and create a locally verified replacement Recovery Kit. Persist only the kit and a copy of the new root key encrypted to the current device key so a browser restart can resume safely.
- Rewrap each record data key with
CryptoEngine.rewrapRecordDataKeyand persist through an application-provided record callback. - Save monotonic progress and an opaque application checkpoint so interruption can resume.
- Wrap the new root key independently for every active device public key.
- Atomically replace all active device envelopes, Recovery Kit ID, and vault key version, then mark the rotation completed.
- Destroy the old root-key handle only after the final transaction succeeds.
The RotationState type is a discriminated union. An in-progress state cannot contain completion
metadata. A completed state requires completedAt and recoveryKitId. Every write uses an
optimistic revision. Key versions must advance by exactly one.
Checkpoint safety
checkpoint is an opaque string supplied by the host record repository. It may contain a cursor or
record identifier, but must never contain record plaintext, a root key, a data key, a recovery
password, or Recovery Kit contents. processedRecords is monotonic and is progress metadata visible
to the server.
Atomic completion
Completion must reject missing, duplicated, stale, revoked, wrong-vault, wrong-public-key, or wrong-key-version device replacements. The vault and every active device must change in one database transaction; partial cutover would make some devices unable to unlock the current key.
Browser API
Rotation is deliberately a two-step user flow. First generate and download the replacement kit:
const kit = await vault.prepareRootKeyRotation(replacementSecret)
if (kit.status === "ok") RecoveryKitFile.download(kit.value)
After the user imports that exact file, pass its serialized contents and a host record store to completion:
await vault.completeRootKeyRotation({
recoveryKit: importedContents,
secret: replacementSecret,
records: encryptedRecordStore,
batchSize: 100,
})
React applications call useKeyRotation(encryptedRecordStore). Preparing a rotation returns the
replacement kit without downloading it. The host must save and re-import that exact file before
completion. Completion requires imported kit
contents and its replacement password; neither is sent through an Ownfold transport.
Use useKeyRotationProgress() for a stable external-store snapshot. Its discriminated union is
idle, pending, or completed. Pending progress identifies the awaiting-recovery-verification,
rewrapping-records, updating-devices, or committing phase and includes absolute processed
record count plus the source and destination key versions. The browser equivalent is
getRotationProgress() with subscribeRotationProgress(listener).
The record-store contract does not provide a total record count, so Ownfold deliberately renders an indeterminate progress indicator and an exact processed count rather than inventing a percentage. On restart, progress resumes in the Recovery Kit verification phase; remote checkpoint progress is reconciled before the next batch.
RotationRecordStore.loadBatch returns opaque record IDs, envelopes, an absolute processed count,
and a resumable checkpoint. saveBatch must atomically and idempotently replace that batch. The
client accepts already-rewrapped V2 records when replaying an interrupted batch, but rejects mixed
vaults and unrelated key versions. Record ciphertext is copied unchanged; only wrapped data keys
are replaced.
While rotation is pending, normal encrypt, decrypt, and pairing operations fail with
ROTATION_IN_PROGRESS. Other tabs lock through a secret-free lifecycle message. A restarted browser
unlocks with the current device key or old Recovery Kit, then resumes using the saved replacement
kit. The replacement secret is never persisted or transported.
The core contracts, strict parsers, in-memory implementation, and PostgreSQL, Drizzle, and Prisma transactions are implemented. The official adapters persist monotonic checkpoints and reject a partial or stale device-envelope cutover. Authenticated server methods, Web/fetch routes, the fetch transport, and tRPC procedures carry the same typed lifecycle. Browser orchestration, Recovery Kit replacement, restart recovery, idempotent record batches, and cross-tab access guards are now implemented and tested. The browser client and headless React hooks cover replacement-kit creation, local re-import, resumable completion, and actionable failure states. Typed phase and processed-record progress is available through the browser client and headless React hook. A required scale gate creates and rotates 4,097 encrypted records in 128-record batches, injects an interruption after a saved batch, resumes idempotently, verifies every wrapped key and unchanged content ciphertext, decrypts boundary samples, checks the plaintext-free transport boundary, and enforces a 30-second rotation-operation budget. Independent security review remains before rotation is production-ready.
Interrupted completion
A client may not know whether a request failed before or after the server committed it. Rotation
retry therefore treats checkpoints, record writes, and final completion as idempotent operations.
The test transport can inject both pre-operation failures with failNext(operation) and lost
success responses with failNextResponse(operation).
Automated tests cover a failure after a rewrapped batch was saved but before its progress response, and a failure after the atomic vault/device cutover committed but before the client received the response. In the latter case, retry reads the completed remote rotation, validates its IDs and key versions, finalizes local encrypted state, and only then destroys the old root-key handle.