KeyRing
Purpose
A member's client-side secret store — the epoch keys they were let into, and nothing anyone else can open. At rest the ring is sealed under a key derived from the member's passkey, so the content is unreadable even with the device in hand until the passkey is presented; the keys never leave the device in the clear. The passkey ceremony itself is the boundary's carried concern — the domain owns that a ring at rest yields nothing, and opens only through the ceremony. Asking the ring for a key it does not hold is not an error — it is the access model working: an epoch you were never let into is an epoch you cannot read.
Attributes
- owner: the
Memberthe ring belongs to - group keys: the
GroupKeysthe owner holds, one per epoch they were accepted into - state: is one of "sealed", "opened" that says whether the ring is at rest or in use — the Machine's state
Invariants
(none) — the one invariant this draft carried ("a ring whose state is 'sealed' yields nothing") is now two rows of the Machine below, each of which gates its operation directly. An authored invariant that restates a machine row is redundant by construction.
Operations
unlock
Produces a new KeyRing, opened — the same keys, unreadable no longer. Only the owner's passkey ceremony reaches this operation; how the passkey is presented and verified is the boundary's carried concern. Result: self
seal
Produces a new KeyRing, sealed — at rest, its keys unreadable until the owner's passkey opens it again. Result: self
add group key
Produces a new KeyRing also holding a given GroupKey. It fails when the ring is at rest — a ring
at rest takes no key.
Result: self
key for
Reports the GroupKey, if any, the ring holds for a given GroupRevision — that epoch's key;
nothing when the owner was never let into the epoch.
Result: the GroupKey the ring holds for that epoch, if any
Machine
The ring's rest/use lifecycle — every operation's legality in one table. A ring begins sealed.
- state: state
- start: sealed
| from | on | do | to | reason (refuse only) |
|---|---|---|---|---|
| sealed | unlock | permit | opened | |
| sealed | seal | permit | sealed | |
| sealed | add group key | refuse | a ring at rest takes no key | |
| sealed | key for | answer nothing | ||
| opened | unlock | permit | opened | |
| opened | seal | permit | sealed | |
| opened | add group key | permit | opened | |
| opened | key for | permit | opened |
The examples
Context: a KeyRing owned by karin.dahl holding the GroupKey "k-0163", and the GroupRevision
"g-7421.3" under key id "k-0163", created by karin.dahl and naming karin.dahl among its members;
a GroupKey "k-0150" for the adding rows.
- each transition — the operation named, from a ring in the block's state
Scenarios
An opened ring yields the epoch's key
Given: an opened KeyRing owned by karin.dahl holding the GroupKey "k-0163", and the GroupRevision "g-7421.3" under key id "k-0163"
When: key for that revision is asked
Then: the ring yields the GroupKey "k-0163"
An epoch never let into yields nothing
Given: an opened KeyRing owned by karin.dahl holding the GroupKey "k-0163", and the GroupRevision "g-7421.4" under key id "k-0264"
When: key for that revision is asked
Then: the ring reports nothing — an epoch she was never let into is an epoch she cannot read
An opened ring gains a key
Given: an opened KeyRing owned by anne.bruun holding the GroupKey "k-0142"
When: add group key is performed with the GroupKey "k-0150"
Then: the result is a ring holding both "k-0142" and "k-0150"