Skip to content

Phase 2 plan: personal use

Status: plan v1, all seven milestones complete on 2026-09-04 · Exit condition from Architecture §8: drills of total loss and restore; enrollment never silent, with multi-device, identity kit, archive and revocation.

Phase 1 left a CLI messenger that a family could run on a LAN with one device per person. Phase 2 makes it survive the things that happen to people: a second device, a lost phone, a stolen laptop, a fresh install after total loss, and a group whose creator disappears. Every mechanism follows Protocol §8 and §9 and the recovery split of ADR-006: identity recovery, device enrollment and history archive are three separate objects with separate keys.

Scope

In: linking a second device signed by the root on the administration device; one MLS leaf and one route per device, fan-out to every device of every member including one's own; revocation by manifest, enforced on the relay and inside groups; a deterministic successor for the commit coordinator (v0.3 review §3.1); the identity kit and the history archive in a reviewed format; recovery from total loss with server rollback detection; encryption of the client database at rest.

Out (Phase 3): the production pairing protocol over a live channel with QR and transcript codes (Phase 2 links through a copied bundle over a channel the user already trusts), GUI, push, resumable uploads, social recovery, delegation of the root's signing power.

Milestones

Milestone Deliverable Acceptance
M2.1 Device linking device request on the new device (keys only, no root); device authorize on the administration device (root signs the credential and manifest N+1, publishes both to the relay); device link on the new device (verifies the grant binds its own keys, enrolls as a member, prints its route). Relay: credential_put and manifest_put verified against the member's root, sequence must advance, credential must be listed active Script: alice's second device joins the realm without any invite and without the root leaving the first device; a grant for other keys is refused; a manifest that skips or repeats a sequence is refused by the relay; the relay log records the enrollment
M2.2 Multi-device conversations Routes carry the device id, credential hash and root key (arveil-route:v1); peers stored per device; KeyPackages claimed per device; fan-out to every device of every member, own devices included; chat add accepts a device of an existing member Script: bob sends once, both of alice's devices read it; alice's laptop sends, her phone and bob read it; both alice devices show the same history
M2.3 Revocation device revoke signs manifest N+1, publishes it, sends a manifest event into every group; the relay refuses the revoked device's handshake and invalidates its mailbox; members verify the manifest under the known root, stop sending to the device, and the coordinator removes its leaf; every other member pauses sending until the Remove is applied Script: alice revokes her laptop; the laptop can no longer connect nor be written to; bob refuses to send while the leaf is present, then sends after the Remove; the laptop's stored state cannot read anything after the new epoch
M2.4 Coordinator succession GroupPolicy v2: the authorized committer is the lowest active leaf; a commit that removes the current lowest leaf is accepted from the next one only if it carries, in its authenticated data, a manifest signed by that leaf's root revoking its credential Script: alice (creator) revokes her phone (leaf 0) from her laptop; bob's device, now the lowest active leaf, removes the phone; every member accepts the commit; a commit from a non-lowest leaf is still refused
M2.5 Identity kit and history archive kit export writes the root seed and manifest state encrypted to a fresh age X25519 identity, printing the secret once; kit restore on a clean client recovers the identity, compares the manifest chain with the relay (a lower or forked version on the relay is reported, never overwritten), revokes the lost devices and enrolls the new one; archive export/archive import move events and downloaded files under a separate age secret and import them as archived records with no MLS state Script: total loss drill; a relay restored from an older snapshot is detected (I-08); the archive imports history that the new device never received and it never becomes a new event (I-07); the kit and archive files contain no plaintext
M2.6 Encryption at rest Client database opened through SQLCipher with a key from ARVEIL_DB_KEY; the same key covers the MLS provider's tables, the WAL and the outbox Script: the database file holds no plaintext, route or identity id; a wrong key fails to open; the demo runs unchanged with a key set
M2.7 Phase 2 exit scripts/phase2.sh in CI, this document updated with results, README roadmap, protocol notes in both languages CI green with every script

Design notes fixed for this phase

  • Link grant. device authorize prints arveil-link-grant:v0:<hex CBOR {credential, manifest, root_public}>. The new device accepts it only if the credential names its own device id and keys and verifies under root_public, and only if identity_id derives from that root. The grant carries no private material. The relay learns the new device from the administration device (member session), so the new device's first connection is already a member handshake. Copying the grant over a channel the user trusts is the Phase 2 substitute for the pairing protocol.
  • Manifests as events. A manifest travels inside groups as an MLS application event of kind manifest. Receivers accept it only under the root they already know for that identity (from the route) and only if it advances the sequence they know; rollbacks and forks are visible errors. On every sync a client also refreshes the latest manifest of each peer identity from the relay; a relay that hides versions is caught by the in-group copy, and the reverse.
  • Revocation inside a group. A device known to be revoked receives nothing more. If this device is the authorized committer it removes the leaf at once; otherwise sending is paused (chat send fails with a visible reason) until an epoch without that leaf is applied. Pausing is the documented cost of not trusting the relay for freshness.
  • Successor rule. The committer is the lowest leaf index present in the tree. Removing that leaf needs proof that its identity revoked it: the successor puts the signed manifest in the commit's authenticated_data, and every receiver verifies it under the root and credential hash it stored for that leaf. No election, no relay sequencing.
  • Kit and archive format. Both use the age file format with X25519 recipients: the secret shown to the user is the age identity (AGE-SECRET-KEY-1…), high entropy by construction, no password KDF. The kit holds {version, root_seed, identity_id, manifest_state} in CBOR; the archive holds {version, identity_id, exported_at, events[], files[]}. Neither contains device private keys nor MLS state.
  • Recovery ordering. Restore first proves the root, then reads the relay's newest manifest for the identity, then signs manifest N+1 that revokes every previously active credential and activates the new one. Groups are rejoined by a fresh chat add from each coordinator; the archive is imported afterwards as history, never as authority.

Results

All acceptance rows are exercised by scripts/phase2.sh, which runs in CI.

  • M2.1 A second device joins without an invite and without the root leaving the administration device: device request prints public keys only, device authorize signs the credential and manifest N+1 and publishes both, device link accepts the grant only if it names its own keys. A grant for other keys is refused, a used grant cannot link a second device, and the realm registers a credential only when its newest manifest already lists it, logging the enrollment.
  • M2.2 arveil-route:v1 carries the device id, credential hash and root key, and the identity id must derive from that root. Peers are stored per device, KeyPackages are claimed per device, and a message fans out to every device of every member, one's own included. Bob's message reaches both of Alice's devices; her laptop's message reaches her phone and Bob; both her histories match.
  • M2.3 device revoke publishes manifest N+1 and sends it into every group. The realm refuses the revoked device's handshake and revokes its mailboxes' capabilities. Members that are not the committer pause sending, with the reason, until the leaf is gone; chat remove lets the committer enact a revocation it verified. Manifests are accepted only under the root already stored for that identity, from the group and from the realm, so each catches what the other hides.
  • M2.4 The group's creator is revoked and the next active leaf takes over: it removes the revoked leaf and every member accepts the commit, with the conversation intact. A leaf that is not the lowest active one still cannot commit, and a commit that leaves a revoked leaf in place is refused. The rule is a function of authenticated state, not an election.
  • M2.5 Total loss drill: kit and archive exported, the device destroyed, the identity recovered on a clean client, the group removing the lost leaf before adding the recovered device, which cannot read anything from before its Add. The archive fills that gap as archived history, is never re-sent and is idempotent on a second import. Against a realm restored from an older snapshot, recovery reports the rollback with what to do about it (I-08). Neither file contains plaintext.
  • M2.6 With ARVEIL_DB_KEY set to 64 hex characters the whole conversation works unchanged and the client database has no SQLite header, no message, no identity, no device id and no route. A wrong key, a short key and no key each fail with their own message; an unkeyed database says it is unkeyed.

A note on issue numbers. The tracking issues for this phase were lost to a tooling error and recreated afterwards (#40 to #46). The Closes #NN lines in the phase 2 commits therefore point at numbers that were later reused by phase 3 issues; the milestones and this document are the reliable record of what each commit did.

What Phase 2 leaves open, for Phase 3: the pairing protocol over a live channel with QR and transcript confirmation (Phase 2 copies a grant over a channel the user already trusts), the GUI, push, resumable uploads, social recovery and delegating the root's signing power.