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>
33 KiB
AF — Encryption (at-rest + E2EE) + Hardware Keys
Goal
Two security floors land together because they share the key-management spine:
-
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.
-
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.
-
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-copilotis 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-bossproxy if available, or via the server-side wrapped-DEK path (§AF6) — the recipient's ai-copilot gets the DEK and can call@model-bossserver-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-bossruns 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_surfaceJSONB → column-level encryptedjournal_entries.body,journal_entries.title(brief Q) → column-level encrypted; never decryptable by platform (§AF3)prospects.preferred_languageis plaintext (used by AD sticky);prospects.pii_*columns (govt name candidates, hotel addresses, raw phone, raw email) → column-level encryptedpeer_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 → encryptedpeer_collaborations.payload_json→ encryptedpeer_endorsements.evidence→ encryptedcoop_reports.notes,coop_reports.attachments→ already encrypted per brief N §N1 (preserved; AF tightens key-management to match)agent_actions.outcome_jsonwhen it contains plaintext drafts or PII → row flagpayload_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 hashedorgs.*,org_members.*— org topology metadatasettings.*— surface configs (which surfaces are active, posture flags) — operationalagent_actionsmetadata columns (action_type, timestamps, stakes, confidence, target_id) — required for audit replay + analyticscontent_plans.surface,content_plans.planned_for,content_plans.status— operational; the draft text inside plans is encrypted (plan_jsonper 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-secretextension (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):
- Not enrolled (default) — sign-in uses passphrase + device key. Settings shows "Add a hardware key — recommended" card.
- Enrolling primary — tap key, name it (e.g., "yellow YubiKey 5 NFC"), confirm test sign-in.
- Enrolling backup (strongly nudged) — second key, named, tested.
- Enrolled (≥ 1 key) — sign-in path adds YubiKey assertion step.
- Lost primary — sign-in with backup key, settings flags primary as "missing", prompts to revoke and enroll replacement.
- 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:
- New device generates Device-Identity-Key.
- Existing device displays a one-shot pairing code + scans / receives a QR.
- Existing device signs the new device's key with User-Identity-Key (YubiKey tap if enrolled).
- New device pulls all encrypted blobs (peer-thread DEKs wrapped to its new device key, persona-DEK, journal-DEK).
- 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_wrapsrow markedrevoked_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_wrapsrows markedrevoked_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.triagegets prospect-PII-DEK + engagement-event ciphertext.prospect-resolvergets 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.
Related
- 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-bossproxy 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 /translatecall works inside the user's server-wrap auth scope without leaking plaintext to platform-as-operator.