Public Access
Merge branch 'claude/team-key-share-rotate-4b2619'
Two conflicts, and the second is worth recording. main's M4 bullet gained the Android signing decision while this branch rewrote the M5 line either side of it; both are kept. The other is an ADR number collision: two sessions each took 0010, one for vault key rotation and one for Android distribution, and both are now on main. ADR numbers are identifiers — "see ADR 0010" appears in code comments as well as in prose — so leaving two would make every such reference ambiguous. The rotation ADR landed first and is referenced from crypto.md, the gaps document, ADR 0009 and the sync code; the Android one is referenced from README and android-port.md. So the later and cheaper one moves: 0010-android-distribution.md is now ADR 0011, with its title and both references updated. Nothing about either decision changes.
This commit is contained in:
@@ -69,25 +69,47 @@ member is the mirror image: every team vault this machine can open is wrapped to
|
||||
add. Both report per vault, including what they could not do — a vault whose key this machine does
|
||||
not hold is skipped and stays flagged, because somebody else has to finish it.
|
||||
|
||||
## What this deliberately does not do
|
||||
## The second half: re-sealing what is already stored
|
||||
|
||||
**It does not re-encrypt what is already stored.** After a rotation the vault's existing items remain
|
||||
sealed under the generations they were written with. The person who left keeps whatever plaintext
|
||||
they already pulled — that is the non-retroactive limit ADR 0001 records and no design here changes
|
||||
it — and, if they kept the old vault key and later obtained ciphertext they had not already
|
||||
downloaded, that ciphertext would still open to them.
|
||||
> **Added 2026-08-04.** This was deferred when the decision above was taken, and is now built. The
|
||||
> reasoning that made it safe to defer is what made it cheap to add, so it is recorded here rather
|
||||
> than in an ADR of its own.
|
||||
|
||||
So the guarantee this buys is exact and worth stating in those words: **everything written from the
|
||||
rotation onwards is unreadable to them.** Nothing about the past changes. The product says that
|
||||
rather than the reassuring version, and the honest remediation for a departure is still to rotate the
|
||||
credentials themselves.
|
||||
A rotation on its own re-keys the vault and not its contents, which leaves one gap: somebody who left
|
||||
with a copy of the old key could still open old ciphertext they later got hold of. `VaultResealer`
|
||||
closes it by walking the vault and rewriting each item under the current key, as an ordinary upsert
|
||||
against the version the server holds.
|
||||
|
||||
Re-sealing the vault's existing items under the new key is the remaining half and is deferred. It is
|
||||
safe to add incrementally *because* of the decision above: mixed generations are readable, so a pass
|
||||
that migrates items one batch at a time cannot strand anything, and a pass that fails half way leaves
|
||||
a vault that still works. Building it the other way round — bumping the generation only once every
|
||||
item had been re-sealed — would have needed the whole vault to move in one transaction, which is a
|
||||
request-size limit dressed as an architecture.
|
||||
Four properties, each of which is a decision:
|
||||
|
||||
- **It never decodes the plaintext.** An item is opened and the *same bytes* are sealed again under a
|
||||
fresh data key. No codec, no merge, no schema version — so an item written by a newer client
|
||||
survives untouched, where re-encoding it through this build's codec would silently drop the fields
|
||||
this build has no concept of. It is also why one pass covers every item type, including types added
|
||||
after it was written.
|
||||
- **It is resumable, and needs no transaction.** Each item is one upsert, so a pass that dies half way
|
||||
leaves a vault at mixed generations — which is a state that reads perfectly well, because that is
|
||||
precisely what the decision above bought. Running it again picks up what is left.
|
||||
- **A conflict is counted, not merged.** The pass changes no content, so there is nothing to merge:
|
||||
an item somebody else wrote meanwhile is left at their version and re-sealed on the next pass.
|
||||
- **A queued local edit is left alone, and re-sealed on the way out instead.** Rewriting it here would
|
||||
overwrite the user's unpushed work with the version the server holds. Instead `SyncEngine` re-seals
|
||||
a queued payload whose generation is stale as it dispatches it, and writes the revision back to the
|
||||
outbox first so a retry sends the same bytes. That closes the one hole a pass over *stored* items
|
||||
cannot see: a change made before the rotation and pushed after it would otherwise put a brand-new
|
||||
item into the vault under the key the departed member holds.
|
||||
|
||||
The pass runs as the last step of a rotation, after a sync — a mirror that is behind produces a batch
|
||||
of conflicts rather than a re-sealed vault. The interface reports which of the two guarantees was
|
||||
reached, because they are different: a vault fully re-sealed is closed to the person who left, and one
|
||||
where items were left behind is closed only to what happens next.
|
||||
|
||||
## What this still does not do
|
||||
|
||||
**It does not reach what they already pulled.** The person who left keeps whatever plaintext is on
|
||||
their machine — that is the non-retroactive limit ADR 0001 records and no design here changes it. The
|
||||
honest remediation for a departure is still to rotate the credentials themselves, and the product says
|
||||
so rather than the reassuring version.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ADR 0010 — Distributing the Android client, and who holds the release key
|
||||
# ADR 0011 — Distributing the Android client, and who holds the release key
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-08-03
|
||||
Reference in New Issue
Block a user