Drafts the Friend-of-Friend post-gating spec with crypto specifics marked TBD — OPUS for Opus to fill in. Six-layer implementation plan; each layer independently shippable. Includes README overview + six layer files: - Layer 1: V_me vouch primitive (keys, keyring, VouchGrant wire format) - Layer 2: Mode 2 — public post + FoF-gated comments - Layer 3: Mode 1 — FoFClosed (encrypted body via wrap_slots + prefilter) - Layer 4: per-post keypair rotation - Layer 5: unlock cache + prefilter optimization (perf-critical) - Layer 6: revocation (stub; likely deferred post-v1) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.5 KiB
Layer 1 — Vouch Primitive
Scope: Introduce V_x vouch keys, per-persona keyring storage, and a distribution/exchange mechanism. No post-gating yet — this layer ships first so the primitive is in place and UI-exercised before any post encryption depends on it.
Goal
After Layer 1 ships:
- Each persona owns a current
V_mesymmetric key. New personas auto-generate one at creation. - Each persona has a keyring of received vouch keys:
{V_x : x has vouched for me}. - Users can view who they've vouched for, who has vouched for them, and revoke/rotate
V_me. - Wire protocol can transfer
V_mefrom voucher to vouchee inside an existing encrypted channel. - No post encryption or comment gating depends on Layer 1 yet — that arrives in Layer 2/3.
Lead decisions
V_meis symmetric, not asymmetric. Every vouchee holds the sameV_me. This is the property that makes FoF reach emergent (wrap underV_x, anyonexvouched for decrypts).- Distribution is unilateral. Alice vouching for Bob = Alice gives Bob a copy of
V_alice. No request/accept handshake. Bob can immediately read FoF posts that includeV_aliceas a wrap slot. - Revocation = rotate
V_me. There is no per-vouchee revocation. To un-vouch someone, the persona generates a newV_me'and re-distributes to every remaining vouchee. Layer 6 stub. - Epoch tag is part of the key. Every
V_xhas an associated(owner_id, epoch)so receivers can tell fresh from stale when multiple copies arrive (e.g., if voucher rotated). - Keyring is per-persona, not per-device. Multi-persona users have independent keyrings. Layer 3 reader logic iterates personas when trial-decrypting.
Data model
vouch_keys_own table
Per-persona, stores the persona's own V_me history (current + recent-past for graceful rotation).
TBD — OPUS: exact column types, key size in bytes, how many past epochs to retain.
vouch_keys_own(
persona_id BLOB,
epoch INTEGER,
key_material BLOB, -- TBD — OPUS: symmetric key bytes (32B for a 256-bit key?)
created_at_ms INTEGER,
is_current INTEGER, -- 1 for the active V_me, 0 for retained past epochs
PRIMARY KEY (persona_id, epoch)
)
vouch_keys_received table
Per-persona, stores vouch keys received from other personas (one row per (owner_id, epoch) currently held).
vouch_keys_received(
holder_persona_id BLOB, -- whose keyring this entry belongs to
owner_id BLOB, -- the persona who owns (issued) this V_x
epoch INTEGER,
key_material BLOB,
received_at_ms INTEGER,
PRIMARY KEY (holder_persona_id, owner_id, epoch)
)
Readers compute their keyring as SELECT key_material FROM vouch_keys_received WHERE holder_persona_id = ? UNION SELECT key_material FROM vouch_keys_own WHERE persona_id = ? AND is_current = 1.
Key generation
TBD — OPUS: specify the primitive. Candidates:
- Random 32B from CSPRNG (simplest;
V_meis pure symmetric secret) - HKDF-derived from persona identity key + epoch counter (allows deterministic re-derivation; couples key rotation to identity key exposure)
Lead leaning: random 32B CSPRNG, stored encrypted at rest alongside identity key material. Identity-key coupling offers no defensive benefit given the keyring is already indexed per-persona in the same DB.
Wire format for vouch distribution
A VouchGrant message carries a copy of the voucher's current V_me from voucher to vouchee.
TBD — OPUS: exact byte layout. Target shape:
VouchGrant {
voucher_persona_id: NodeId, -- sender's persona
epoch: u32,
key_material: [u8; 32], -- V_voucher at this epoch
issued_at_ms: u64,
sig: [u8; 64], -- voucher's identity-key signature over the above
}
Delivery: wrap VouchGrant inside the existing Direct (encrypted-to-one-recipient) post primitive. Do not introduce a new top-level control message. Content-type byte distinguishes vouch grants from regular DMs. Recipient's receive-path decodes, verifies signature, inserts into vouch_keys_received.
TBD — OPUS: decide whether vouch grants should use a reserved visibility intent (VisibilityIntent::VouchGrant) for filtering out of the Messages tab, or whether they ride under Direct and are suppressed client-side by content-type.
UI / UX
Minimum viable surface for Layer 1 ship:
- Persona screen: "Vouch for someone" button. Picker of contacts. Hands them a
V_me. - Persona screen: "Who has vouched for me" list (reads
vouch_keys_receivedgrouped byowner_id). - Persona screen: "People I've vouched for" list (local-only; TBD — OPUS on whether to track this explicitly or derive from DM-sent history — see open question below).
- Settings: "Rotate my vouch key" button → generates new epoch, queues re-distribution to tracked vouchees.
Layer 1 ships without any post/comment behavior change. Vouches are visible in UI but don't gate content yet.
Open questions
- Do we track outbound vouches explicitly? Option A: local
vouches_issued(recipient_persona_id, epoch_at_grant)table. Option B: derive from DM-sent history searching forVouchGrantpayloads. A is simpler + survives DM history loss; B avoids a redundant record. Lead leaning: A. - Re-distribution on rotation. When persona rotates
V_me, do we auto-queueVouchGrantDMs to every tracked vouchee, or require user-initiated re-vouch? Lead leaning: auto-queue with a confirmation summary ("rotate will re-send vouch to N people"). - Key size. 256-bit symmetric key is standard and matches our ChaCha20-Poly1305 usage. Confirm with Opus there's no reason to prefer 192 or 128 bits.
- Epoch granularity. Monotonic counter per persona, or wall-clock-based? Counter is simpler; wall-clock aids debugging. Lead leaning: counter.
- Should
V_meever be exported in the persona backup bundle? LosingV_memeans every FoF post gated under it becomes permanently unreadable for every vouchee. Lead leaning: yes, include in identity export.
Ship criteria for Layer 1
- All personas auto-generate
V_meat creation. - Users can vouch and receive vouches end-to-end via DM-wrapped
VouchGrant. - UI shows received and issued vouches per persona.
V_merotation works; re-distribution to tracked vouchees is demonstrated.- No change to post visibility / comment behavior.
- Integration test: two personas on two devices, Alice vouches for Bob, Bob's
vouch_keys_receivedcontainsV_alicewith correct signature.