cocottetech/@platform/codebase/@features/ai-copilot/docs/AF-encryption-and-hardware-keys.brief.md
natalie 1b719e1fd7 chore(bootstrap): initial V4 commit
Clean successor to V3 (forge: lilith/atlilith). Seeded from local Mac
working tree at ~/Code/@projects/@cocottetech/. node_modules and build
artifacts excluded via .gitignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:11:41 -07:00

33 KiB

AF — Encryption (at-rest + E2EE) + Hardware Keys

Goal

Two security floors land together because they share the key-management spine:

  1. At-rest encryption (sensitive-only) — every sensitive column / object on the persistence tier is encrypted such that database backups, MinIO bucket dumps, or stolen storage media yield no plaintext. Transparent to providers + clients.

  2. End-to-end encryption for the two domains the platform has the strongest privacy stance on:

    • Journals (brief Q) — zero-knowledge to the platform. Even the operators of CocotteAI cannot read a provider's journal.
    • Provider-to-provider data (per brief AE) — peer DMs, salon messages, peer collaboration payloads (co_tour, cross_promo, shared_playbook), mentor specialist-draft shares, peer endorsement evidence. E2EE between connected peers; the platform sees ciphertext only on the network + at rest.
  3. Hardware-key support (YubiKey via WebAuthn / FIDO2) — the user's master key can be unsealed by a hardware token rather than a passphrase, raising the bar against device-compromise + phishing.

The product surface remains opaque per AD: providers + clients never see "encrypted" badges, no "🔒" chrome, no toggles for the at-rest path. Encryption is a substrate, not a feature.

Designer skim

  • Headline UX: A new provider sets up CocotteAI. They get a master key derived from a device passphrase + (optional, strongly recommended) YubiKey. Their journal entries are written through the on-device encryption path; the platform stores ciphertext and never has the key. Peer DMs negotiate a per-thread key wrapped to both peers; salons negotiate a per-salon key with each member's keypair. ai-copilot mediation of peer DMs uses per-user-server-wrapped DEKs by default (each side's ai-copilot holds its user's wrap, so it can decrypt + draft + filter + translate without the platform-as-operator seeing plaintext — see §AF6). YubiKey setup adds a "tap your key to unlock" hearth-register moment at sign-in; the rest of the day, Cocotte just works.
  • Sections (11): AF1 threat model + invariants · AF2 at-rest scope + mechanism · AF3 E2EE scope + mechanism · AF4 key hierarchy + derivation · AF5 hardware-key support (YubiKey) · AF6 ai-copilot mediation under E2EE · AF7 cross-device handoff under E2EE · AF8 recovery + lost-key flows · AF9 audit interplay · AF10 erasure interplay (cryptographic erasure) · AF11 specialist scoping.
  • Foundation: every AF mechanic interacts with AE (peer data), Q (journals), I (audit), V (erasure), W (org context), L (specialists), M (degraded modes), AD (opacity). AF defines the cryptographic substrate; the rest of the corpus inherits it.
  • Voice: this brief = working register. The product itself stays in default register everywhere; the YubiKey setup is hearth (one warm welcome moment) + working (operational steps).
  • Blocking Qs: AF-Q1 at-rest mechanism, AF-Q2 ai-mediation-under-E2EE shape, AF-Q3 YubiKey requirement (mandatory vs strongly-recommended), AF-Q5 erasure semantics.

Constraints

  • Sensitive-only at rest — the directive scope. We do NOT encrypt every column. Settings, surface configs, audit metadata (action_type, timestamps, IDs, stakes labels) stay plaintext for query + ops + analytics. The encrypted set is enumerated in §AF2.
  • Journals are zero-knowledge. No platform operator, no on-call engineer, no admin tool, no backup-restore path can yield plaintext journal entries without the user's key. Recovery without the user's key = data loss (cryptographic erasure equivalent).
  • Peer-shared data is E2EE between the connected peers. The platform stores ciphertext + key-wrap metadata. Each peer's ai-copilot is treated as "part of the user" via per-user-server-wrapped DEKs (default mediated path) — see §AF6.
  • AD opacity preserved. No "🔒 encrypted" badge, no "this message is end-to-end encrypted" annotation, no language-naming moments. Encryption is invisible.
  • Audit append-only preserved (brief I). Audit rows for encrypted content store ciphertext refs + wrap metadata, not plaintext. Erasure = key destruction; the audit row stays as a tombstone with redacted payload.
  • K3 PII gates run on plaintext on the encrypting party's device (provider's iOS / Mac), not on the platform. The platform never sees the plaintext to gate.
  • Voice §V6 banned-phrase enforcement also runs on-device for E2EE channels, since the platform can't see the text.
  • AD opacity multilingual translation for E2EE peer DMs runs on the recipient's device via local @model-boss proxy if available, or via the server-side wrapped-DEK path (§AF6) — the recipient's ai-copilot gets the DEK and can call @model-boss server-side with plaintext-on-its-own-behalf. Either way, the platform-as-operator never sees the plaintext outside the recipient's own ai-copilot scope.
  • @model-boss runs on apricot and must continue to be the inference router. Per CLAUDE.md: never load models locally on iOS. (On-device sketch for AF6 uses the per-device proxy that holds the wrap-key; inference is still apricot-side.)
  • No commits from agents (apricot ACS). Platform-action skills upstream.
  • Hardware key is optional but strongly encouraged (AF-Q3 lean). The product works without one; UX nudges the user toward enrolling a YubiKey after first vigil.

States to design

  • Master-key creation at first run (D persona-seed): passphrase + device key + optional YubiKey enroll.
  • YubiKey enrollment flow (first key + backup key).
  • Sign-in: passphrase only · passphrase + YubiKey · YubiKey-only (passkey, post-P0).
  • Journal write/read (on-device crypto, transparent).
  • Peer-DM key negotiation on first message (colleague-state activation).
  • Salon key rotation when a member joins / leaves (forward secrecy posture per AF-Q4).
  • Cross-device handoff (iOS ⇄ Mac ⇄ web): key sync via per-device wrap.
  • Lost-device: existing keys still valid on remaining devices; new device requires re-enrollment + key import.
  • Lost YubiKey: backup YubiKey path · passphrase fallback path · social recovery (P5+ per AF-Q4).
  • Erasure (brief V V2): key destruction → ciphertext becomes opaque → audit tombstone.
  • Compromised device suspected: revoke device key → wrapped-DEKs on that device invalidated → other devices unaffected.
  • ai-copilot mediating an E2EE peer-DM (default, §AF6 — server-side wrapped-DEK).
  • Pure E2EE peer-DM (opt-in per AE-Q4, P5+) — no server-side mediation; on-device only.

AF1 — Threat model + invariants

In scope (what AF defends against):

  • Stolen backup tarballs / DB dumps / MinIO bucket exfiltration.
  • Compromised storage media (apricot disk, black backup volume, vps-0 cache snapshots).
  • Coerced platform operator with database read access — they see ciphertext for encrypted columns.
  • Cross-provider data plane leak — peer A's keys never decrypt peer B's data.
  • Cloud-storage backup-restore by a third-party vendor — they see ciphertext only.

Out of scope (what AF doesn't defend against):

  • Active compromise of a user's device after sign-in (keys are unsealed in process memory).
  • A user voluntarily sharing their YubiKey + passphrase with a third party.
  • ai-copilot itself being compromised (it's "part of the user"; if it's owned, the user is owned).
  • Side-channel attacks on the inference layer.
  • Subpoena of ciphertext (we comply with ciphertext production; we cannot produce plaintext for E2EE content).

Hard rules:

  • No plaintext journals at rest on platform.db, MinIO, Redis cache, backups, or vps-0 cache rebuilder. Period.
  • No plaintext peer-DM bytes on platform.db outside per-user server-wrapped DEK access scope (§AF6).
  • Cryptographic erasure is real erasure (brief V V2): key destruction + tombstone is the canonical "deleted" state for E2EE data.
  • At-rest key material never leaves apricot HSM / black KMS boundary for server-side wrapped DEKs.

AF2 — At-rest encryption (sensitive-only)

Mechanism (per AF-Q1 lean): envelope encryption with per-field Data Encryption Keys (DEKs) wrapped by per-user Master Keys (MKs). MKs themselves are wrapped by either (a) the user's device key + passphrase or (b) the user's YubiKey. Server-side DEK wraps are stored alongside ciphertext.

Encrypted set (the "sensitive" enumeration):

  • personas.preferred_voice_facets, personas.kinks, personas.off_limits, personas.brand_voice_per_surface JSONB → column-level encrypted
  • journal_entries.body, journal_entries.title (brief Q) → column-level encrypted; never decryptable by platform (§AF3)
  • prospects.preferred_language is plaintext (used by AD sticky); prospects.pii_* columns (govt name candidates, hotel addresses, raw phone, raw email) → column-level encrypted
  • peer_messages.original_text, peer_messages.canonical_text, peer_messages.recipient_view_text, peer_messages.delivered_text → encrypted (per §AF6 wrap scheme)
  • peer_group_messages.* text columns → encrypted
  • peer_collaborations.payload_json → encrypted
  • peer_endorsements.evidence → encrypted
  • coop_reports.notes, coop_reports.attachments → already encrypted per brief N §N1 (preserved; AF tightens key-management to match)
  • agent_actions.outcome_json when it contains plaintext drafts or PII → row flag payload_encrypted=TRUE, payload stored as ciphertext blob
  • MinIO content assets (provider's uploaded shoots, brand assets) → envelope-encrypted per-object with object-level DEK

Unencrypted set (explicitly plaintext):

  • users.* (id, email-hash, created_at) — metadata only; email is hashed
  • orgs.*, org_members.* — org topology metadata
  • settings.* — surface configs (which surfaces are active, posture flags) — operational
  • agent_actions metadata columns (action_type, timestamps, stakes, confidence, target_id) — required for audit replay + analytics
  • content_plans.surface, content_plans.planned_for, content_plans.status — operational; the draft text inside plans is encrypted (plan_json per row flag)
  • coops.* topology
  • Mailbox configs (brief P)

AF3 — E2EE scope + mechanism

Two E2EE domains:

AF3a — Journals (zero-knowledge)

  • Brief Q established that journal entries are "PRIVATE to Quinn" — never published, never quoted in outbound.
  • AF makes this cryptographic: journal entries are encrypted on the provider's device with a journal-DEK that is wrapped only by the user's Master Key.
  • The platform never holds an unwrapped journal-DEK.
  • Erasure = destroy the journal-DEK (cryptographic erasure).
  • Recovery = if the user loses their Master Key, journal data is unrecoverable.

AF3b — Peer-shared data

  • Peer DMs (AE3), salon messages (AE4), peer collaboration payloads (AE6), mentor specialist-draft shares (AE8), peer endorsement evidence (AE7).
  • Per-thread / per-salon symmetric key (Thread-DEK) negotiated via X3DH-style handshake (Double Ratchet for DMs, sender-key for salons per Signal's group protocol).
  • Each member's keypair is per-device; per-user identity key signs device keypairs.
  • Forward secrecy default for peer DMs (DEK rotates per session per AF-Q4).
  • Salon key rotation on member join/leave (forward secrecy with delete-after-leave).
  • The platform stores: ciphertext + per-recipient wrap of Thread-DEK + protocol metadata. No plaintext.

Library choice: vendor-neutral; lean on libsignal for the protocol primitives (Double Ratchet, X3DH, sender-key). Swift on iOS, native bindings on macOS, WebCrypto + libsignal-wasm on web companion.

AF4 — Key hierarchy + derivation

User-Identity-Key (Ed25519, per-user, long-lived)
    └─ signs Device-Identity-Keys (Ed25519, per-device)
            └─ wraps Master-Key (AES-256-GCM, per-device-wrapped)
                    ├─ wraps Journal-DEK (AES-256-GCM)
                    ├─ wraps Persona-DEK
                    ├─ wraps Prospect-PII-DEK
                    └─ wraps Peer-Thread-DEKs (one per peer thread + salon)

Server-side wrapped DEK (for AF6 default-mediated path):
    User-Master-Key (per-user) → wraps Server-Wrap-Key (AES-256-GCM)
        Server-Wrap-Key encrypted to user's server-side identity (HSM-bound on apricot)
        Allows user's own ai-copilot (running with user's auth scope) to decrypt
        Cannot be decrypted by another user, by platform-admin without user's auth, etc.

Master-Key derivation candidates (per AF-Q1):

  • Passphrase + PBKDF2/Argon2id (default fallback)
  • Passphrase + Device Secure Enclave (iOS / Mac) — primary on Apple platforms
  • YubiKey FIDO2 hmac-secret extension — preferred when enrolled (AF5)

AF5 — Hardware-key support (YubiKey)

Why: hardware-bound keys defeat phishing and most credential-theft attacks; the platform's "supervised autonomy" model deserves a strong-as-possible authentication floor.

Supported tokens:

  • YubiKey 5 series (USB-C, NFC, Lightning) — primary
  • Apple Passkeys on Secure Enclave — equivalent for non-YubiKey-using providers
  • Generic FIDO2 / WebAuthn tokens — supported via standard protocol

Use cases inside CocotteAI:

  • Sign-in second factor at sign-in (replaces / augments TOTP).
  • Master-Key unseal at sign-in via FIDO2 hmac-secret extension (the hardware key derives a high-entropy secret per registration; we use that to wrap/unwrap the Master Key).
  • Per-session re-auth for high-stakes actions (K3 PII override, V2 erasure confirm, key recovery operations) via FIDO2 user-presence assertion.
  • Cross-device key transport: when adding a new device, the YubiKey tap on the existing device authorizes the new device's identity-key signing.

Enrollment flow (states):

  1. Not enrolled (default) — sign-in uses passphrase + device key. Settings shows "Add a hardware key — recommended" card.
  2. Enrolling primary — tap key, name it (e.g., "yellow YubiKey 5 NFC"), confirm test sign-in.
  3. Enrolling backup (strongly nudged) — second key, named, tested.
  4. Enrolled (≥ 1 key) — sign-in path adds YubiKey assertion step.
  5. Lost primary — sign-in with backup key, settings flags primary as "missing", prompts to revoke and enroll replacement.
  6. All keys lost — fallback: passphrase + Master-Key-Recovery-Code path (see §AF8).

Voice (settings + onboarding nudges):

  • (hearth, post-first-vigil) "You're getting traction. Lock things down — add a hardware key when you get a chance."
  • (working, enrollment) "Tap your key. Name it. Tap again to confirm."
  • (working, backup nudge) "Second key strongly encouraged. If your first key disappears, the second one keeps you in."
  • (plain, lost-key) "Sign in with your backup key. Revoke the missing one from settings after."

AF6 — ai-copilot mediation under E2EE (the load-bearing decision)

The brief's hardest move: peer DMs are E2EE between peers, but AE3's default is ai-mediated (each side's ai-copilot drafts/filters/translates). These look incompatible. They aren't, because each user's ai-copilot is part of the user — not a third party.

Mechanism — Server-side wrapped DEK (default per AF-Q2 lean):

Each user has a server-side User-Wrap-Key stored on apricot HSM (encrypted with user's Master Key at provision-time). The user's ai-copilot runs server-side in the user's auth scope; when it needs to decrypt the user's own data (e.g., to draft a peer DM, ingest an incoming peer DM, write a journal-search index entry locally), it requests the User-Wrap-Key be unsealed via apricot HSM, decrypts the per-thread DEK, and processes plaintext on the user's behalf, inside the user's auth boundary.

This is NOT a backdoor:

  • The User-Wrap-Key is wrapped to the user's Master Key — it cannot be unsealed without user-provided credentials at sign-in.
  • The HSM enforces the auth scope: a request to unseal user A's User-Wrap-Key requires user A's active session.
  • Platform-admin cannot unseal any user's key without that user's active session.
  • The HSM logs every unseal — append-only audit.

Trade-off acknowledged: this is "E2EE between peers, with each peer's ai-copilot as a trusted endpoint." A purist would call this E2E2EE (end-to-end-to-ai-endpoint). We're explicit about it. The pure peer-to-peer no-mediation path is AE-Q4 opt-in (P5+) — with the caveats AE3 already listed (lose AD opacity translation, lose K3 server-side gate, lose voice §V6 enforcement).

On-device alternative: a P5+ track where @model-boss proxies run on-device (iOS Neural Engine, Mac Apple Silicon) and the user's ai-copilot never sends plaintext to apricot. Out of scope for P0; tracked as AF-Q6.

AF7 — Cross-device handoff under E2EE

User has iOS + Mac + web companion. Each device has its own Device-Identity-Key (Ed25519), signed by the User-Identity-Key.

When the user adds a new device:

  1. New device generates Device-Identity-Key.
  2. Existing device displays a one-shot pairing code + scans / receives a QR.
  3. Existing device signs the new device's key with User-Identity-Key (YubiKey tap if enrolled).
  4. New device pulls all encrypted blobs (peer-thread DEKs wrapped to its new device key, persona-DEK, journal-DEK).
  5. New device is now a full peer.

Sync via existing platform.api cache.invalidate event bus + per-device wrap-blobs in device_wraps table (new in migration 0007).

AF8 — Recovery + lost-key flows

Three recovery paths, in increasing severity:

AF8a — Lost device, other devices OK

  • Sign in on a remaining device.
  • Revoke missing device's identity-key (device_wraps row marked revoked_at).
  • Re-wrap peer-thread DEKs to the remaining devices.
  • New device pairing as in §AF7.

AF8b — Lost primary YubiKey, backup YubiKey OK

  • Sign in with backup key.
  • Settings → "Replace primary key" → enroll new key.
  • Old primary device_wraps rows marked revoked_at.

AF8c — All keys + devices lost (catastrophic)

  • Master-Key-Recovery-Code (12-word BIP39 mnemonic, generated at first run, user printed/stored offline).
  • Recovery code re-wraps the User-Master-Key on a new device.
  • Without the recovery code: data is lost. Cryptographic erasure occurred at the moment the keys were lost. We don't sugar-coat this.

Voice for recovery (plain register):

  • "Recovery code restores your Master Key. Without it, your journal and peer history is gone."
  • "We can't recover keys from our side. That's the whole point."

AF9 — Audit interplay (brief I)

Audit rows for E2EE-payload actions store ciphertext + wrap metadata, never plaintext. The audit row's outcome_json is { "ref": "peer_messages:<uuid>", "payload_encrypted": true, "wrap_scheme": "server-wrap-v1" }.

Replay (brief I §replay) for an E2EE action:

  • If the user is signed-in → ai-copilot decrypts the ciphertext on user's behalf (per §AF6) → user sees plaintext in the audit-row-detail screen.
  • If the user is offline → audit row shows ciphertext placeholder ("Encrypted payload — sign in to view").

Audit append-only is preserved. Encryption doesn't change the spine.

AF10 — Erasure interplay (brief V)

Cryptographic erasure becomes the canonical erasure for E2EE data:

  • V2 erasure cooling-off still applies (30-day grace).
  • At end of cooling-off, the per-user Master Key wrap is destroyed (or the relevant per-domain DEK is destroyed — finer-grained erasures possible).
  • Ciphertext remains in peer_messages, journal_entries, etc., but is unrecoverable.
  • Audit row supplements with erased_at + tombstone metadata.
  • Periodic compaction job (P5+) physically deletes ciphertext rows older than N months after erasure.

For at-rest-only (non-E2EE) sensitive data: erasure destroys both the DEK wrap and (on next compaction) the ciphertext. Equivalent privacy outcome.

AF11 — Specialist scoping under encryption

Each per-surface specialist (content-onlyfans, content-x, bookings-tryst, etc.) runs within the user's auth scope — same as ai-copilot. They get DEK access via the User-Wrap-Key path for the data they're authorized to touch:

  • content-{surface} specialists get persona-DEK (read voice) + prospect-PII-DEK (for triage) + relevant content_plan/post draft access.
  • bookings-* specialists get prospect-PII-DEK + tour-data DEK.
  • triage gets prospect-PII-DEK + engagement-event ciphertext.
  • prospect-resolver gets prospect-PII-DEK across surfaces (still RLS-isolated per provider per Y).

Specialists do not get journal-DEK (journals are user-private per AF3a; no specialist reads journals — even ai-copilot reads them only on explicit user request).

Specialists do not get peer-thread DEKs unless the user has explicitly added the specialist to a peer interaction (e.g., a mentor sharing a sanitized specialist-draft to the mentee per AE8 — the share is a new artifact, re-encrypted to mentor + mentee, not the original peer-thread DEK).

Schema follow-up (sketch for migration 0007)

Authoritative migration filename: @platform/infrastructure/sql/migrations/0007_encryption_and_keys.sql.

-- user identity + master key
CREATE TABLE user_keys (
  user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
  identity_pubkey BYTEA NOT NULL,           -- Ed25519
  master_key_wrap_passphrase BYTEA NULL,    -- Argon2id-wrapped MK
  master_key_wrap_yubikey BYTEA NULL,       -- FIDO2 hmac-secret-derived wrap
  recovery_code_hash BYTEA NULL,            -- hash of BIP39 mnemonic for AF8c
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  rotated_at TIMESTAMPTZ NULL,
  CHECK (master_key_wrap_passphrase IS NOT NULL OR master_key_wrap_yubikey IS NOT NULL)
);

-- per-device wraps
CREATE TABLE device_wraps (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  device_pubkey BYTEA NOT NULL,             -- Ed25519, signed by user identity key
  device_name TEXT NOT NULL,                -- "Quinn's iPhone 17"
  device_kind TEXT NOT NULL,                -- ios | mac | web
  master_key_wrap BYTEA NOT NULL,           -- MK wrapped to device pubkey
  enrolled_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  last_seen_at TIMESTAMPTZ NULL,
  revoked_at TIMESTAMPTZ NULL,
  UNIQUE (user_id, device_pubkey)
);

-- hardware keys (YubiKey + Passkey)
CREATE TYPE hw_key_kind AS ENUM ('yubikey', 'passkey', 'fido2_other');
CREATE TABLE hardware_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  kind hw_key_kind NOT NULL,
  name TEXT NOT NULL,                       -- "yellow YubiKey 5 NFC"
  credential_id BYTEA NOT NULL,             -- WebAuthn credential ID
  pubkey BYTEA NOT NULL,                    -- WebAuthn credential pubkey
  aaguid UUID NULL,                         -- authenticator attestation GUID
  attestation_format TEXT NULL,
  is_primary BOOLEAN NOT NULL DEFAULT FALSE,
  is_backup BOOLEAN NOT NULL DEFAULT FALSE,
  master_key_wrap_via_hmac BYTEA NULL,      -- MK wrap via FIDO2 hmac-secret
  enrolled_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  revoked_at TIMESTAMPTZ NULL,
  CHECK (is_primary OR is_backup OR (NOT is_primary AND NOT is_backup))
);

-- server-side wrap key (for AF6 default-mediated path)
CREATE TABLE server_wrap_keys (
  user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
  wrap_ciphertext BYTEA NOT NULL,           -- HSM-bound, wraps user's server-side DEKs
  hsm_keyref TEXT NOT NULL,                 -- apricot HSM key reference
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  rotated_at TIMESTAMPTZ NULL
);

-- peer-thread DEKs (E2EE)
CREATE TABLE peer_thread_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  thread_kind TEXT NOT NULL,                -- 'peer_dm' | 'peer_group' | 'mentorship'
  thread_id UUID NOT NULL,                  -- references peer_connections.id / peer_groups.id / peer_mentorships.id
  rotated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE peer_thread_key_wraps (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  thread_key_id UUID NOT NULL REFERENCES peer_thread_keys(id) ON DELETE CASCADE,
  recipient_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  wrap_ciphertext BYTEA NOT NULL,           -- thread-DEK wrapped to recipient's device or server-wrap key
  wrap_target TEXT NOT NULL,                -- 'device:<id>' | 'server'
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (thread_key_id, recipient_user_id, wrap_target)
);

-- column-level encryption flags
-- (no new table; existing tables get encrypted-payload flags + ciphertext columns)
-- e.g., agent_actions.outcome_json becomes agent_actions.outcome_ciphertext BYTEA + outcome_dek_ref UUID
-- migration 0007 ALTERs the relevant tables to add ciphertext columns + drops plaintext where applicable

RLS unchanged on existing per-provider tables; new key tables require current_user_uuid() match.

In-the-wild copy

  • (hearth, post-first-vigil nudge) "You're getting traction. Lock things down — add a hardware key when you get a chance."
  • (working, enrollment) "Tap your key. Name it. Tap again to confirm."
  • (working, backup-key nudge) "Second key strongly encouraged. If your first key disappears, the second one keeps you in."
  • (plain, lost-key recovery) "Sign in with your backup key. Revoke the missing one from settings after."
  • (plain, catastrophic loss) "We can't recover keys from our side. That's the whole point. Recovery code is the only path back."
  • (working, audit-row-detail when offline) "Encrypted payload — sign in to view."
  • (working, journal-write) (no chrome — journal write is plaintext-on-device, encrypted under user's MK before persist; no banner)
  • (plain, K3 leak in E2EE peer DM) "Held a draft back — contains restricted content. See audit for the row." (same opacity-preserving copy AE3 + AD use)

Edge cases

  • Provider has only one device + no YubiKey + forgets passphrase + no recovery code printed — data is cryptographically lost. We surface this risk loudly at first-run setup; we do not soften the loss.
  • YubiKey lost during international travel — backup YubiKey path (§AF8b); if both gone, passphrase + recovery code path (§AF8c).
  • Provider gives ai-copilot temporary admin access to debug an issue — out of scope; we don't have a "share my session" feature. If we ever add one, AF requires explicit per-session DEK delegation with the user's YubiKey tap.
  • Cross-device race on key rotation — last-writer-wins on rotated_at; old wraps invalidated server-side; clients with stale wraps re-fetch.
  • Org-context (W) + E2EE peer-DM — peer-DM at P0 is personal-only (per AE-Q6 lean). When org-attributed peer-DMs ship at P5+, the org's keypair wraps the org-attributed peer-thread DEKs separately from personal.
  • VoiceOver during sign-in with YubiKey — accessible flow per brief X; "Tap your YubiKey on your phone" + audio confirmation when key is read.
  • Reduced motion — no special-case; encryption flows have no animation beyond standard system sheet transitions.
  • iOS Cellular Watch sign-in (per brief AB AB-Q5) — Watch holds a device-key wrap; sign-in via passphrase on Watch is awkward → AB-Q5 leans phone-tethered for high-security operations.
  • K3 PII gate fires on outbound peer DM (E2EE) — gate runs on sender's device pre-encryption. Block + audit row. Same UX as AE3 §K3 path.
  • AD low-confidence translation fallback on E2EE peer DM — translation runs on recipient's ai-copilot side via the server-wrapped DEK path (§AF6); recipient still gets confidence-framed fallback per AD7.
  • Backup-restore drill (engineering) — restoring a DB backup yields ciphertext only for encrypted fields; restored state is operational once a user signs in and unseals their keys. Backup-restore does NOT have a privileged decryption path.
  • Brief Q — journal entries are zero-knowledge per AF3a; AF makes the existing privacy stance cryptographic.
  • Brief AE §AE3 + §AE-Q4 — peer DM E2EE shape; AF6 resolves the ai-mediation tension via server-side wrapped DEKs.
  • Brief N §N1 + §N7 — coop reports already encrypted per N7; AF tightens key-management to the unified hierarchy.
  • Brief V — cryptographic erasure becomes the canonical erasure for E2EE data per AF10.
  • Brief I — audit append-only preserved; encrypted-payload rows store ciphertext refs.
  • Brief W §W4 — org-attributed E2EE peer-DM deferred to P5+ per AE-Q6.
  • Brief D — first-run flow includes key creation + YubiKey nudge.
  • Brief S — settings root adds Security section for hardware-key management.
  • Brief AB — Watch sign-in interplay with YubiKey constraints.
  • Brief X — VoiceOver flow for YubiKey enrollment + sign-in.
  • Brief AD — opacity invariant preserved; encryption is invisible to the surface.

Out of scope

  • Maximalist zero-knowledge (all data encrypted, on-device inference only) — explicitly rejected per user direction. Defer the on-device-only path to a P5+ exploration tracked as AF-Q6.
  • Sharing across providers without a peer connection — AE invariants hold; AF doesn't add new cross-provider data planes.
  • Subpoena bypass / lawful-access backdoors — not a feature; not a path.
  • Encryption of unencrypted set columns (settings, audit metadata, surface configs) — operational/analytics need plaintext; encryption-at-rest at the OS / filesystem layer (LUKS, FileVault) is sufficient.
  • YubiKey-required posture (mandatory hardware key) — strongly encouraged, not required, per AF-Q3 lean.
  • Social recovery (peer-co-attested key recovery) — P5+ exploration tracked as AF-Q7.

Open questions

  • AF-Q1 At-rest encryption mechanism — envelope encryption with per-field DEKs (current lean) vs filesystem-level (LUKS / FileVault dominant) vs hybrid? [blocking] (lean: envelope encryption + filesystem LUKS as belt-and-braces; envelope encryption is what gives us per-user erasure semantics).
  • AF-Q2 ai-copilot mediation under E2EE — server-side wrapped DEK (current §AF6 default) vs on-device-only (P5+ path)? [blocking] (lean: server-side wrapped DEK at P0; on-device-only is AE-Q4 + AF-Q6 future track).
  • AF-Q3 YubiKey requirement posture — mandatory for E2EE features, strongly-encouraged, or optional? [blocking] (lean: strongly-encouraged; product works without; UX nudges after first vigil).
  • AF-Q4 Forward-secrecy semantics — Double Ratchet for peer DMs (full FS) vs sender-key for salons (FS on rotation) vs nothing (simpler)? [engineering] (lean: Double Ratchet for AE3 DMs, sender-key for AE4 salons with rotation on join/leave).
  • AF-Q5 Cryptographic erasure compaction — physically delete ciphertext after N months post-key-destroy, or keep ciphertext forever? [exploratory] (lean: compaction job in P5+ for storage hygiene; key destruction is the real privacy event).
  • AF-Q6 On-device-only mediation — when does the pure peer-to-peer no-server-wrap path ship? [exploratory] (lean: P5+; requires on-device @model-boss proxy infrastructure).
  • AF-Q7 Social recovery (peer-attested key recovery) — does AE peer-connection imply any social-recovery capability? [exploratory] (lean: no — keep social recovery out; recovery is YubiKey + passphrase + recovery code only).
  • AF-Q8 Backup-restore privileged-access posture — engineering can restore backups but cannot decrypt encrypted fields; is there ever a "break-glass" scenario? [blocking] (lean: no; the absence of a break-glass IS the security guarantee).
  • AF-Q9 YubiKey enrollment timing — D persona-seed (first-run) or post-first-vigil (current §AF5 nudge)? [blocking] (lean: post-first-vigil; first-run is already dense; nudge once the user trusts the platform enough).

Apricot-deferred verifications

Per [[feedback-apricot-unreachable]] — apricot is reachable again per session-start MCP reconnect. The following verifications are now unblockable when implementation begins:

  • HSM key management on apricot — does the existing GPU host have HSM capability (or do we use a cloud HSM like AWS KMS) for §AF6 server-side wrap keys?
  • FIDO2 hmac-secret round-trip on iOS + macOS + web companion — confirm WebAuthn library support for hmac-secret extension across all three surfaces.
  • Envelope encryption performance on platform.db — measure column-level encryption overhead on query paths that scan encrypted JSONB columns (personas, prospect-PII).
  • Double Ratchet library (libsignal) integration on Swift + WASM — confirm protocol primitives work cross-platform.
  • Backup-restore drill — restore a DB backup to a test instance, verify ciphertext-only state, verify no privileged-access path exists.
  • K3 PII gate on-device — measure latency of on-device PII detection (replaces server-side gate for E2EE channels).
  • AD opacity translation under E2EE — verify recipient-side @model-boss /translate call works inside the user's server-wrap auth scope without leaking plaintext to platform-as-operator.