# pvtcoms — Design & Build Plan

> A private, serverless, peer-to-peer, **anonymous**, **post-quantum** encrypted messenger.
> Cross-platform (Android phone + desktop PC). Legally neutral infrastructure — the user is responsible for use.
>
> Status: **design / brainstorm stage** (no code yet). This document reconciles independent input from
> Claude (web research, 2025–2026), **Codex**, and **Gemini**, plus three prior design rounds.
> Last updated: 2026-05-30.

---

## 0. The one-paragraph summary

pvtcoms is closest in spirit to **SimpleX Chat**, but pushes further on two axes: a **serverless DHT-rendezvous**
discovery layer (instead of operator-run relays) and a **Rust** core that ships **post-quantum** crypto from day one.
We borrow heavily from proven designs (SimpleX SMP, Signal PQXDH/Triple-Ratchet, Briar's pull-not-push, MLS for groups),
and we are realistic about the three things that genuinely cannot be wished away: the anonymity-vs-mobile-battery
trilemma, first-contact MITM, and mobile push notifications. Where those bite, we ship **honest UX modes**, not magic.

---

## 1. Non-negotiables (what the product IS)

1. **No central server owns your messages.** Discovery is decentralized; message storage is oblivious.
2. **Anonymous & unlinkable.** No phone number, no email, no global user ID. Rotating, shared-secret-derived addresses.
3. **Post-quantum.** Encryption that resists a future quantum computer ("harvest-now-decrypt-later" defeated).
4. **Cross-platform.** One Rust core; Android + desktop from the same codebase.
5. **Legally neutral.** "Protocol as math." No built-in illegal features; clear acceptable-use posture; user bears responsibility.

---

## 2. The honest constraints (designed around, not denied)

These came up independently from Gemini and the research. Internalize them before building.

| Constraint | The brutal truth | Our answer |
|---|---|---|
| **Anonymity ⊕ instant ⊕ battery** | Pick two. Persistent Tor/mixnet circuits murder mobile battery; the OS kills background sockets. | Two **UX modes**: *Battery-Saver* (pull on open + OS background fetch, delayed) and *Always-On* (foreground service, instant, battery cost). Never a *transport* toggle. |
| **Mobile push notifications** | FCM/APNs need a stable device token + a server that knows you're reachable → kills anonymity. Without them, iOS freezes the app and Android kills it. **This is the single most likely reason the project fails.** | Anonymous mode = no FCM/APNs, rely on pull. Optional convenience push with an explicit metadata warning. Accept "feels less instant than WhatsApp." |
| **First-contact MITM** | Remote introduction with zero trust is *mathematically impossible*. Whoever relays the first key exchange can be the MITM. | TOFU + **unskippable SAS (safety-number) verification** for sensitive contacts; explicit per-contact trust provenance (`scanned` / `sas-verified` / `introduced-by-X` / `link-only`). Don't pretend encrypted = authenticated. |
| **Direct P2P leaks your IP** | A raw peer connection exposes your IP to the peer and both ISPs — destroys anonymity (this is Tox's central failure). | Never connect "directly" on the clear net. All connections ride the anonymity transport (Tor now, Nym later). |
| **Traffic correlation** | Even E2E + Tor, a global observer can correlate timing/volume. SimpleX has *not* shipped its randomized-latency fix; it's the standing metadata hole. | Fixed-size blocks (16 KB), randomized delivery latency/jitter **from day one**, cover traffic in high-anon mode. |

---

## 3. Reconciled architecture

Layered, with a clean trait boundary between the **message layer** (where the PQ guarantee lives) and the
**transport layer** (treated as an untrusted pipe).

```
┌─────────────────────────────────────────────────────────────┐
│  UI:  Kotlin/Compose (Android)   |   Tauri + web UI (Desktop) │
├─────────────────────────────────────────────────────────────┤
│  pvtcoms-core (Rust)                                          │
│   • Identity & device log (Ed25519+ML-DSA-65, cross-signed)   │
│   • Sessions: hybrid PQ Double Ratchet (X25519+ML-KEM-768)    │
│   • Groups: Sender-Keys v1 → OpenMLS X-Wing v2                │
│   • Mailbox: SimpleX-style unidirectional rotating-token queue│
│   • Rendezvous: shared-secret + epoch → rotating token        │
│   • Anti-abuse: PoW gate + local web-of-trust                 │
├─────────────────────────────────────────────────────────────┤
│  AnonTransport trait  → arti (Tor v3 onion) | Nym (later)     │
└─────────────────────────────────────────────────────────────┘
```

### 3.1 Identity & keys
- **Two-layer identity.** A **master identity key** (`Ed25519 + ML-DSA-65`) that *never touches the network* — it only
  signs. Plus rotating ephemeral session keys.
- **Identity is pairwise.** N contacts = N independent secrets, channels, ratchets, and rotating tokens. There is no
  global "you" visible to the network.
- **Verification.** Signal-style safety number + a key-continuity chain so a key rotation can't be silently hijacked.

### 3.2 Cryptography
- **Handshake (mature, commit now):** hybrid **X25519 + ML-KEM-768** — the PQXDH pattern. Gives harvest-now-decrypt-later
  protection for session setup. ✅ Battle-tested (Signal 2023, Apple PQ3 2024).
- **Ongoing ratchet (the frontier):** put the **KEM inside the Double Ratchet**, not just the opening handshake — this is
  what buys *quantum-safe post-compromise security*. This is genuinely new (Signal's SPQR shipped Oct 2025). Build it as a
  **pluggable, version-gated module**, not a launch dependency.
  - **Bandwidth strategy decision:** prefer **PQ3-style periodic PQ rekey** (~every N messages / ≤7 days) over SPQR's
    per-message chunk-streaming for v1 — it integrates far more cleanly with an offline/pull mailbox model. Revisit
    SPQR's erasure-coded "ML-KEM Braid" if per-message PCS granularity becomes a requirement.
- **Signatures:** `Ed25519 + ML-DSA-65` for **identity/prekey signing only**. Keep them **OUT of the per-message ratchet**
  — they're ~3.3 KB and **non-deniable**. Both SimpleX and Signal deliberately keep the ratchet signature-free to preserve
  deniability. (Don't repeat Session V1's mistake of dropping PFS/deniability.)
- **AEAD:** ChaCha20-Poly1305 (or XChaCha20 for nonce-misuse resilience in async contexts). Fast on phones without AES-NI.
- **KEM choice note:** we pick ML-KEM-768 (FIPS 203, formally-verified Rust impls, smaller keys, faster keygen than
  SimpleX's sntrup761 — which fixes SimpleX's ~10–20 keygen/sec group-size ceiling). Document SimpleX's counter-argument
  (ML-KEM removed a hashing step; RNG-failure concern) and mitigate by hashing transcripts into the KDF.

### 3.3 Transport (anonymity layer)
- **v1 = Tor v3 onion services via `arti`.** Arti hit **stable 2.0.0 (2026-02-02)** with real onion-service support and
  **restricted discovery** (stabilized 1.7.0) — which maps perfectly onto rotating rendezvous tokens (each token → an
  ephemeral, client-authorized onion address). Pure-Rust, audited, ~monthly releases. **Avoid Briar's mistake** of one
  onion service per contact (O(contacts), slow, battery-heavy).
- **v2 = Nym mixnet, behind a trait.** Only thing that defeats a *global passive adversary*, but adds ~500–800 ms/message
  and its PQ/Outfox story is unproven. Optional, future, swappable — never couple our PQ guarantee to Nym's roadmap.
- **Not for anonymity:** MASQUE/QUIC relays (Apple/Cloudflare) are 2-hop operator-trust — usable only as a censorship-
  circumvention wrapper *under* Tor, never as the anonymity layer.
- **Abstraction:** one `AnonTransport` Rust trait (`establish/send/recv`) so arti / Nym / future backends are swappable.

### 3.4 Discovery & rendezvous (the "find each other again and again" problem — solved)
- **Shared secret + time epoch → rotating token:** `token = HMAC(MasterSecret, epoch)`. Both peers derive the same
  meeting point each epoch; to the rest of the DHT it's noise. Public addresses rotate (anonymity) while known contacts
  still re-find each other (continuity). **Never publish real keys to the DHT.**
- **Threat-model the DHT explicitly** — this is our riskiest novel component (SimpleX deliberately avoided DHT in favor of
  transparent relays). Lookups must not reveal *who* is being looked up or *who* is asking; tokens must be unlinkable
  across rotations. Design against enumeration, eclipse, and Sybil. Tox's pre-0.2.2 onion-routing IP-leak CVE is the
  cautionary tale.
- **Implemented (2026-05-31, `core::directory`, ADR-006):** **directory records** carry this out over the oblivious relay
  (not a DHT yet). From the pairwise root `R`: `dir_token = HMAC(R,"dir-loc"‖pub‖epoch)` (directional, per-epoch),
  `dir_key = HMAC(R,"dir-key"‖pub)` — **key-separated** from each other and from message keys. The record
  `{identity, seq, expires_at, addresses[]}` (onion + optional direct IP) is **signed** by the Ed25519 identity, **sealed**
  under `dir_key`, deposited under `dir_token`. Epoch+seq **synthetic nonce** ⇒ unlinkable across epochs even for unchanged
  content; monotonic `seq` + `expires_at` give anti-rollback/freshness; the reader verifies against the **pinned** identity.
  Only the intended contact (holder of `R`) can locate, decrypt, or verify. *Remaining:* derive `R` from the handshake +
  persist per contact; relay-client + GUI wiring; consent (friend-request) flow.

### 3.5 Delivery — direct-first, oblivious-mailbox fallback (refined 2026-05-30)
**The mailbox is NOT "the cloud."** It is a dumb relay holding a **sealed E2E-encrypted blob** addressed to a **rotating
one-time token** — it cannot read content, sender, or recipient, cannot link the token to an identity, deletes on pickup, and
is **self-hostable** (run your own or a contact's). Think *left-luggage locker for a sealed box addressed to a code*, not
*readable mail on a company server*.

**Three-tier delivery (Claude decision):**
1. **Direct, peer-to-peer over Tor when both are online** — no relay touches the message. (This honors "deliver straight
   between users" whenever possible.)
2. **Oblivious-mailbox fallback when the recipient is offline** — sender drops the sealed blob and *sleeps*; recipient pulls
   it on next wake. This is why store-and-forward exists: **the alternative (direct-only + sender-retry, the Briar model)
   requires BOTH phones online simultaneously AND both running in the background — doubling battery and still delivering
   unreliably.** The mailbox decouples them so **only the recipient** has the background problem.
3. **Strict "direct-only / no relay" toggle** (per-contact or per-account) for users who refuse any blob at rest — accepting
   Briar-style reliability (both must be online).

**Mailbox mechanics:** unidirectional rotating-token queues. Recipient creates queue secret `Q`; per-epoch locator
`token = HMAC(Q,"loc"‖epoch)`, key `KDF(Q,"enc"‖epoch)`. Two-way chat = two one-way queues on different relays. Relay sees
only an opaque rotating token + a fixed-size (16 KB) padded blob; separate send/receive credentials → sender deniability.
Deposit `(token, padded ciphertext, expiry, PoW)` over Tor; recipient blind-pulls, decrypts locally, deletes. Ephemeral —
3-day default / 14-day max; per-token quotas. **Honest caveat:** oblivious rotating-token mailboxes have no audited precedent
(SimpleX uses transparent relays) → research-grade, audit before "stable."

**Implemented (2026-05-31, `core::relay` + `demo/src/mailbox.rs`, deployed via `deploy/`):** the relay policy engine
validates every request for freshness + **access capability** + **PoW** + **replay**, stores with a deposit timestamp, and
ages out by TTL. Two access modes: **Gated** — a *shared* capability (`HMAC(access_key,…)`) gates a private/family relay to
the invited circle **without** per-identity signatures (those would link identity↔token and break obliviousness; the relay
still cannot tell members apart) — and **Open** (PoW-only public). A random per-request **salt** is bound into the PoW +
capability so a legitimate same-second re-pull is not a false replay while verbatim/mutated replays are still caught. Daemon:
keep-alive framing, atomic on-disk snapshot persistence (mail survives restart), periodic TTL/replay sweep. Production posture:
**relay is onion-only** (reached over Tor; firewall denies all inbound but SSH). Rationale + trade-offs: **ADR-005**.

### 3.5a Background delivery & wake (the "works like WhatsApp" problem)
Because only the **recipient** needs to wake-and-pull, this is a per-device problem. **Privacy is the default; convenience is
opt-in with disclosed costs.**
- **Default = Battery-Saver (pull):** sync when the app is open + opportunistic OS background; delayed delivery; **zero push
  metadata.** Always with **jitter + cover traffic** so poll *timing* doesn't leak the daily pattern (the §-security-review leak).
- **Android / Desktop opt-in "Always-On":** persistent **foreground service** (FGS type `remoteMessaging`, uncapped — NOT
  `dataSync` which Android 15 caps at 6h/24h) holding the Tor connection for near-real-time pull. **Needs NO Google/FCM token —
  privacy-clean** (SimpleX/Briar/Molly model). Cost: ~4–12%/day battery; aggressive OEMs (Xiaomi/Huawei/Samsung — dontkillmyapp)
  may kill it → in-app setup guide + battery-optimization-exemption + `BOOT_COMPLETED` restart + WorkManager(15-min) self-heal;
  honest "best-effort on hostile OEMs." Desktop = a real always-on daemon (no throttling).
- **iOS (v2) — the hard one:** no background sockets; **APNs is the only reliable wake and a documented surveillance honeypot**
  (Wyden 2023: governments subpoena push-token records). So: pull/background-fetch by default; an **oblivious push relay**
  (SimpleX pattern — content-free encrypted wake via APNs → app pulls real message over Tor via a Notification Service
  Extension) is **opt-in** and **OFF by default for high-threat** users. The relay is self-hostable; Apple still unavoidably
  learns device-token + timing, so disclose it. (Tor on iOS needs Orbot-as-VPN; fail-closed if it isn't routing.)
- **No FCM/APNs identity mapping by default.** The single biggest mistake to avoid (both advisors): silently adding push and
  betraying the anonymity users came for. Any push is oblivious, opt-in, self-hostable, and labeled with its metadata cost.

### 3.6 Presence — **pull, never push**
- "Deliver only when the recipient is online" via **active presence detection is itself a metadata leak** (sleep/timezone/
  location + real-time social graph). **Invert it:** the recipient's own device blindly polls its token when *it* decides
  to come online. Same outcome the user wanted, zero presence leak.
- Residual: relay sees poll timing → mitigate with fixed-interval polling + jitter + cover polls.
- **Read receipts** leak "recipient came online" → off by default; if on, delayed/batched/padded via a separate queue.

### 3.7 Groups (built on a pairwise foundation)
This is where Codex and Gemini disagreed; here's the reconciled call.
- **v1 = Signal Sender-Keys.** O(n) fan-out but async-friendly and dead-simple over a P2P/mailbox network; rotate the group
  key on membership change. Gemini is right that **vanilla MLS cannot run serverless** — RFC 9420 needs a single total
  order on Commits; concurrent Commits fork the group state and a DHT has no arbiter. Good enough for groups < ~50.
  *Design storage/epochs now so migration is painless* (Codex's point).
- **v2 = OpenMLS + X-Wing hybrid KEM, hub-mediated.** OpenMLS already ships the `MLS_256_XWING_*` ciphersuite (X25519 +
  ML-KEM-768, formally-verified libcrux). Use a **per-group leader elected via the DHT** to order Commits (Matrix's
  pragmatic MSC4244 retreat). Layer a custom delivery/ordering shim on top of OpenMLS rather than forking its crypto.
- **v3 (research track) = fork-resilient DMLS / FREEK** (`draft-kohbrok-mls-dmls`, currently *expired & unadopted*; PoC
  `HiveNetCode/distributed-mls` explicitly anticipates a DHT addressing layer). Tolerates Commit forks at a forward-secrecy
  cost. Adopt only once matured/audited.
- PQ TreeKEM inflates Commit sizes (painful over Tor/Nym) → adopt the IETF **amortized hybrid combiner** pattern
  (`draft-ietf-mls-combiner`): cheap traditional updates, occasional full hybrid updates.

### 3.8 Multi-device (phone + PC, no central account)
- **Reality check:** *no mature serverless full multi-device sync exists anywhere.* SimpleX refuses it as "a security
  compromise."
- **v1 = host/controller model** (SimpleX XRCP / Gemini's "dumb terminal"): the **phone is authoritative** (holds profiles/
  keys/history); the **desktop is a thin remote** over an encrypted channel. Sidesteps the N-device state explosion. If the
  phone is offline, the desktop is idle. Ships something secure *now*.
- **v2 = self-signed device log + cross-signing.** Master identity key signs each device's key into an append-only,
  version-gated **device log** gossiped over pairwise channels. Borrow Matrix's Master/Self-Signing/User-Signing hierarchy
  but serverless; verification = peer-to-peer SAS. **Invariant (Sesame):** never share a device's ratchet private key —
  each device runs its own ratchet with every peer and with your own other devices (budget the fan-out). New device added
  only via interactive QR + session-code from an already-trusted device (passive "announce a new key" breaks PCS — ePrint
  2021/626).
- **Tamper detection:** a serverless **key-transparency** analogue using `facebook/akd`-style verifiable logs so peers
  detect a swapped device key.
- **Recovery:** user-held BIP39/SSSS recovery secret, and/or experimental **t-of-n threshold** social recovery. Never
  server-vouched.

### 3.9 Anti-spam / abuse resistance (also the legal-neutrality anchor)
- **Gemini's key insight (adopted):** with rotating single-use invite mailboxes, **spam is largely a non-problem** — nobody
  can message you without *your* specific token; if a token is abused, delete and rotate. Spam ≈ solved by a local
  web-of-trust.
- **Gate public/long-lived addresses with adaptive Hashcash PoW** (the only scarce resource when there's no identity).
- **Defer** Privacy Pass / anonymous credentials and **RLN/Semaphore (zk)** — RLN needs a global membership Merkle tree
  (re-introduces consensus/blockchain) and a ~1–5 s mobile ZK proof *per message*. Overengineered for v1; revisit only for
  large public rooms.

---

## 4. What the latest research (2024–2026) changes about our plan

1. **The PQ handshake is mature; the PQ *ongoing ratchet* is ~6 months old.** Commit to the handshake now; ship the PQ
   ratchet as a version-gated module. (Signal **SPQR**, Oct 2025; Apple **PQ3**, Feb 2024.)
2. **`arti` is finally stable (2.0.0, Feb 2026)** with onion services + restricted discovery → our rendezvous layer has a
   production Rust home. Track TROVE advisories (TROVE-2026-005 hit embedded builds like ours).
3. **PQ-MLS is real but unfinished for serverless.** OpenMLS ships X-Wing today; `draft-ietf-mls-pq-ciphersuites` (9 suites)
   and `draft-ietf-mls-combiner` are live, but decentralized MLS (DMLS) is an expired draft. → Sender-Keys v1, hub-MLS v2.
4. **No serverless full multi-device sync exists.** → host/controller v1 is the only honest near-term answer.
5. **Key transparency went mainstream (2024–2026)** on Meta's open-source **AKD** → a serverless KT analogue is now buildable.
6. **Session V2 (Dec 2025)** is adding PFS + ML-KEM but is *unspecified/unproven*; Briar and Tox still have **no PQC**. →
   pvtcoms's committed hybrid PQ would make it **more PQ-mature than all three closest models** — a real differentiator,
   but it means we're ahead of the proven art and must integrate carefully (mirror Signal PQXDH / Apple PQ3).
7. **Traffic correlation stays unsolved** even in audited SimpleX (randomized latency deferred). → ship jitter/cover from
   day one and prioritize the Nym path.

---

## 5. Build vs. fork — verdict

**Compose from audited Rust libraries around our own thin protocol core. Do not fork SimpleX (Haskell) or build our own
anonymity network.**

- **Reuse:** `arti` (transport), `libsignal`/RustCrypto + `liboqs`/`libcrux` (crypto primitives), **OpenMLS** (groups),
  `facebook/akd` (key transparency), and **SimpleX's specs** (SMP/pqdr — specs, not code) as the reference for the
  no-identifier mailbox.
- **Evaluate but mind licensing:** Signal **SPQR** is **AGPL-3.0** — decide pvtcoms's own license before reuse, or treat it
  as a design reference and reimplement.
- **Write ourselves (small, auditable crate):** the rotating-token rendezvous, the oblivious mailbox client, the device log/
  cross-signing, and the glue. Keep crypto in a tiny core crate (emulate Session's `libsession-util` consolidation).
- **Don't build:** a custom DHT anonymity network (3 years of Sybil/eclipse fights to ship a worse Tor — Gemini), or a
  bespoke handshake (Tox's KCI bug, Session's dropped PFS are the warnings).

---

## 6. Tech stack

| Layer | Choice |
|---|---|
| Core | **Rust** (memory-safe; one codebase → Android via JNI/UniFFI, desktop via Tauri) |
| Transport | `arti` 2.x (Tor) now; `nym-vpn-lib` (FFI) later, behind `AnonTransport` trait |
| Classical crypto | RustCrypto (`x25519-dalek`, `ed25519-dalek`, `chacha20poly1305`, `hkdf`) |
| PQ crypto | `liboqs`/`pqcrypto` or **`libcrux`** (formally-verified ML-KEM) for ML-KEM-768 + ML-DSA-65 |
| Groups | **OpenMLS** (+ X-Wing) for v2; hand-rolled Sender-Keys for v1 |
| Key transparency | `facebook/akd` (Rust) |
| Local storage | encrypted SQLite (SQLCipher) or `redb`/`sled` + age-style encryption |
| Android UI | Kotlin + Jetpack Compose over the Rust core (UniFFI) |
| Desktop UI | **Tauri** (Rust + lightweight web UI) |
| Assurance | `hax`→F*/ProVerif modeling of the ratchet; **independent external audit before any "stable" claim** |

---

## 7. Phased roadmap

**Phase 1 — The math (no network).** Rust core: hybrid PQXDH handshake + Double Ratchet, deterministic, with exhaustive
test vectors. Identity keys + safety-number verification. *Exit:* two in-process sessions exchange messages; vectors pass.

**Phase 2 — The tunnel.** Embed `arti`. Client A spins up an ephemeral onion service, Client B connects via a QR-swapped
address. Pipe Phase-1 crypto through it. 16 KB fixed blocks. *Exit:* two real devices chat over Tor with manual contact add.

**Phase 3 — The rendezvous + mailbox.** Shared-secret epoch tokens for discovery; oblivious unidirectional rotating-token
mailbox for offline delivery; pull-not-push polling with jitter; PoW deposit gate. *Exit:* contacts auto-reconnect across
IP changes; offline messages deliver on next poll.

**Phase 4 — The product.** Tauri desktop as host/controller remote; Kotlin Android app; QR contact exchange with trust
provenance UI; two UX modes (Battery-Saver / Always-On). Sender-Keys group chat. *Exit:* daily-usable on phone + PC.

**Phase 5 — Hardening & frontier.** PQ ongoing-ratchet module (PQ3-style periodic rekey); randomized-latency + cover
traffic; serverless key-transparency device log; **external security audit**; begin Nym transport behind the trait.

**Voice/video:** async encrypted **voice/video notes in Phase 4** (Opus 1.6 + SFrame keyed off the ratchet, over the
mailbox — cheap, anonymous, PQ). **Live calls are a v2 track** (Sans-IO media core + SFrame; relay policy per §11.6).

**v2+ research tracks:** OpenMLS hub-mediated PQ groups; self-signed multi-device device-log + threshold recovery; DMLS;
live calling + group calls (SFU + SFrame + MLS).

---

## 8. Risk register (highest first)

1. **Mobile background execution / push** — most likely killer. *Mitigation:* honest dual UX modes; never promise WhatsApp-instant.
2. **Oblivious DHT mailbox is unproven** — no audited precedent. *Mitigation:* threat-model + audit before "stable"; keep transparent-relay fallback in reach.
3. **PQ ongoing ratchet is bleeding-edge** — *Mitigation:* version-gate it; ship mature PQ handshake first; PQ3-style simplicity over SPQR complexity.
4. **First-contact MITM** — unavoidable for remote intros. *Mitigation:* TOFU + mandatory SAS + visible trust provenance; never claim zero-trust remote intro.
5. **Traffic correlation** — global-observer metadata leak. *Mitigation:* jitter/cover from day one; prioritize Nym.
6. **Serverless groups & multi-device are research-grade** — *Mitigation:* ship the simple, proven subset (Sender-Keys, host/controller) first.
7. **Solo-build crypto risk** — *Mitigation:* compose audited libs; tiny core crate; external audit; formal modeling.
8. **Calling pressure → centralized TURN relays** (Gemini: the biggest way calling ruins anonymity) — *Mitigation:* async voice notes as default; never operate central relays; BYO/self-hosted relays only; live = explicit weaker mode.

---

## 9. Open decisions for Rui

- **License?** (Affects whether we can reuse AGPL SPQR. MIT/Apache vs GPL/AGPL.)
- **iOS in scope?** (Briar shows Apple makes always-on impossible; it changes the multi-device & push story.)
- **Deniability a hard requirement?** (It is for SimpleX; it constrains where signatures can go.)
- **Target user:** "privacy-curious daily driver" vs "high-threat activist"? (Sets how much battery/latency we can spend.)
- **Group chat in v1 at all, or 1:1-only first?**
- **Live calls or async voice/video notes only?** And if live: default to **Direct** (reveals IP, no infra) or **Relayed** (self-hosted relay hides IP)? See §11.6.
- **Positioning & MVP feature scope (§12):** lean "uncompromised tool for high-threat/privacy users" (embrace friction) vs "approachable everyday private messenger" (more polish, but resist convenience footguns)? This sets how aggressively we cut features.

---

## 10. Curated repos & papers

**Reference implementations / libraries**
- `signalapp/libsignal` — PQXDH + Double Ratchet (Rust), our crypto blueprint.
- `signalapp/SparsePostQuantumRatchet` — SPQR PQ ratchet (Rust/F*, **AGPL-3.0**), Oct 2025.
- `simplex-chat/simplexmq` (`protocol/pqdr.md`) — no-identifier mailbox + PQ double-ratchet **spec** (Haskell impl).
- `openmls/openmls` — RFC 9420 + X-Wing hybrid PQ groups (Rust, libcrux-verified). | `awslabs/mls-rs` (unaudited, crypto-agile).
- `arti` (gitlab.torproject.org/tpo/core/arti) — Tor in Rust, **stable 2.0.0** Feb 2026.
- `nymtech/nym` / `nym-vpn-client` — Loopix mixnet (future transport).
- `facebook/akd` — Auditable Key Directory (serverless key-transparency building block).
- `HiveNetCode/distributed-mls` — serverless-MLS PoC (DHT addressing) — early-stage.
- Cautionary: `TokTok/c-toxcore` (no audit, IP-leak history), `oxen-io/session-*` (V1 dropped PFS).

**Papers / specs**
- *Triple Ratchet* — eprint **2025/078** (Eurocrypt 2025): SPQR's ML-KEM Braid + erasure-code chunking.
- *iMessage with PQ3* — Apple Security Research, 2024 (Tamarin-verified periodic PQ rekey).
- *SimpleX v5.6 PQ Double Ratchet* (`pqdr.md`, 2024) + **Trail of Bits** design review (Oct 2024).
- **RFC 9420** (MLS) + **RFC 9750** (MLS architecture, Apr 2025, allows decentralized DS).
- `draft-ietf-mls-pq-ciphersuites` (9 suites, 2026) + `draft-ietf-mls-combiner` (amortized hybrid) + eprint 2026/034.
- `draft-kohbrok-mls-dmls-03` (decentralized MLS, expired) + FREEK / FPS-2023 distributed-DS paper.
- *DCGKA* — "Key Agreement for Decentralized Secure Group Messaging," Weidner et al., 2021.
- *Sesame* (Signal, 2017) + *Multi-Device for Signal* (ePrint 2019/1363) + *bad-device PCS break* (ePrint 2021/626).
- *The Loopix Anonymity System* (2017) + *Sphinx* (2009) — mixnet foundations.
- Anti-abuse: Privacy Pass **RFC 9576/9577/9578**; RLN/Semaphore (`semaphore-protocol`, `Rate-Limiting-Nullifier`).

---

## 11. Voice & video calling (added 2026-05-30)

Reconciled from a second three-way pass (Claude research + Codex + Gemini). The headline finding is unanimous and
uncomfortable:

> **Anonymous + real-time live calling is an unsolved frontier. NOBODY ships it today.** Briar refuses calls entirely
> (open issue since 2019). Signal & SimpleX get *metadata privacy* (not anonymity) by relaying media through blind TURN
> servers. Session admits its calls **leak your IP**. Tor carries no UDP and the Tor Project has ruled it out; Nym's
> mixing delays (≥500 ms) blow past the ~150 ms one-way budget for natural conversation. So we split the feature.

### 11.1 The strategy: async-first, live as an explicit weaker mode
- **DEFAULT = asynchronous encrypted voice/video NOTES.** They ride the existing oblivious rotating-token mailbox
  (pull-not-push), so they are **fully anonymous + post-quantum + battery-friendly with essentially zero new transport.**
  Signal itself splits this way (voice notes go through the message path; only live calls use a separate real-time path).
  This is the privacy-correct default and it's cheap to build.
- **LIVE CALLS = an explicit, clearly-labelled weaker-anonymity mode**, never routed over Tor/Nym (the latency makes it
  walkie-talkie at best). Live is Phase-2/opt-in, not a v1 anonymity promise.

### 11.2 Media encryption — SFrame keyed from our existing PQ ratchet (not DTLS-SRTP)
- Encrypt media with **SFrame (RFC 9605, published Aug 2024)** — per-*frame* AEAD (AES-256-GCM), transport-agnostic, so a
  relay/SFU can forward but **never decrypt**. The *same* SFrame-encrypted frame works live **or** stored as a note.
- **Derive the SFrame `base_key` from our hybrid X25519+ML-KEM-768 Double Ratchet via HKDF** — *not* from a WebRTC
  DTLS-SRTP handshake. This is the key insight: it makes calls inherit our identity + PQ guarantees and **sidesteps the
  immature PQ-DTLS situation entirely.**
- **Counter/nonce management is the #1 correctness hazard** (nonce reuse = catastrophic for AES-GCM): tie CTR allocation to
  ratchet steps and re-derive `base_key` on every reconnect/rekey (mobile drops connections constantly).
- SFrame omits per-sender authentication and leaks frame size/timing to the relay → **add per-member ratchet-derived MACs**
  ourselves, and pad/shape frames for traffic-analysis resistance.

### 11.3 "Post-quantum voice" — mostly marketing, and we get the real part for free
- The media stream (AES-256-GCM) is **already quantum-safe** (Grover only halves symmetric strength; NIST excludes
  symmetric from PQ replacement). The only quantum-vulnerable part is the **key exchange** — which our PQ ratchet already
  covers. So **do NOT block on PQ-DTLS 1.3 / X25519MLKEM768 in WebRTC** (real but unfinished in 2026; forcing it inflates
  handshakes to ~10 KB and breaks over lossy UDP). Watch `draft-ietf-tls-ecdhe-mlkem` for later; don't depend on it.

### 11.4 Transport, NAT & the metadata cost
- Live media needs ICE/STUN/TURN, which **leak IP by design** (STUN even bypasses tunnels — it's why Tor Browser disables
  WebRTC). Therefore: **signal/ring the call over the mailbox (latency-tolerant, pull-based), then spin up media only on
  accept.** Don't keep call sockets warm — it wrecks the pull-not-push battery model. Use **Opus DTX** (silence suppression).
- **Force `ICETransportPolicy = relay`; disable host/srflx candidates in privacy mode** so no raw IP is ever exchanged.
- **First-contact / unknown-peer calls = relay-only, NO P2P fallback** (a stranger calling you must never deanonymize your IP).

### 11.5 Codec & Rust stack
- **Opus 1.6 (libopus, Dec 2025) with DRED + Deep PLC** — survives ~90% packet loss at ~1/50 bitrate; the single biggest win
  over lossy paths. Usable at 9–16 kbps. *Vendor libopus 1.6 via FFI* — the Rust `opus`/`audiopus` crates still ship 1.3
  (no DRED/NoLACE). Video: AV1 where available, VP9/H.264 fallback.
- **Use a Sans-IO Rust media core** (`algesten/str0m`, or `webrtc-rs/rtc` as its Sans-IO rewrite matures). Sans-IO decouples
  media frames from the network, so the **same pipeline serves both async notes (over mailboxes) and a future relayed live
  mode**, and isn't locked to UDP sockets (which Tor can't carry). Reuse `TobTheRock/sframe-rs` (pure-Rust RFC 9605, v1.1.0
  Jan 2026) for the media E2EE layer.
- **Group calls (future): SFU + SFrame + MLS** — MLS exporter feeds SFrame `base_key` per epoch; copy Signal's per-frame
  E2EE + key-rotation model (so non-members can't decrypt before joining / after leaving). PQ via X-Wing once stable.

### 11.6 The one real disagreement — live-call default (decision needed)
- **Gemini (purist):** run **no relays at all**. Live = direct clearnet P2P with a screaming "THIS REVEALS YOUR IP TO THIS
  CONTACT" interstitial; if P2P fails (symmetric NAT), the call just fails. Rationale: the moment pvtcoms operates TURN
  relays it becomes a **centralized metadata broker** — "a call is happening between IP A and IP B at time T" — which
  destroys the serverless foundation. *Gemini calls this the single biggest way calling could ruin pvtcoms.*
- **Codex + Signal/SimpleX precedent (pragmatic):** live default = **blind relay-only TURN** (relay sees IP+timing, never
  keys/content), hiding your IP from the *peer*.
- **Proposed synthesis (mine): BYO / community-self-hosted relays.** Offer both as explicit per-call modes —
  **"Direct"** (reveals IP to contact, zero infrastructure) vs **"Relayed"** (hides IP, but a relay you choose sees the
  call happened). Crucially pvtcoms **does not operate the relays** — users/contacts self-host them (SimpleX already
  supports self-hosted ICE/TURN). This threads Gemini's needle (no central metadata broker) while still offering Signal's
  IP-hiding. *Rui to decide the default.*

### 11.7 New repos/specs for calling
- `RFC 9605` SFrame · `TobTheRock/sframe-rs` (pure-Rust, Jan 2026) · `cisco/sframe` (C++ ref).
- `algesten/str0m` (Sans-IO Rust WebRTC) · `webrtc-rs/webrtc` + `webrtc-rs/rtc` (Sans-IO rewrite).
- `signalapp/ringrtc` (Rust call middleware, **AGPLv3** — best prior art, mind license) · `signalapp/webrtc`.
- `xiph/opus` (libopus 1.6 + DRED) · `draft-ietf-mlcodec-opus-dred` · `draft-ietf-tls-ecdhe-mlkem` (X25519MLKEM768, DTLS-OK).
- Research-grade (do NOT ship as default): DONAR "Anonymous VoIP over Tor" (NSDI'22), DarkHorse UDP-for-onion (2023),
  Session's Lokinet UDP onion routing (rolling out).

---

## 12. Features & settings (added 2026-05-30)

Reconciled from a third three-way pass (Claude research + Codex + Gemini). The guiding principle, stated by Gemini and
echoed by the research:

> **Do NOT chase WhatsApp feature-for-feature.** The biggest product mistake here is "trying to be an app for chatting with
> Grandma." Signal drifted that way (payments, stories, cloud-PIN recovery) chasing mass adoption. pvtcoms runs on Tor +
> oblivious mailboxes: it will sometimes be slower, use more battery, and occasionally delay a message. **Embrace that
> friction.** Target privacy-conscious users, journalists, activists, and high-stakes professionals — build one
> uncompromised tool, not a mediocre Swiss-army knife. Privacy is the feature; everything else must not break it.

### 12.1 Feature parity matrix — KEEP / ADAPT / DROP

| Feature | Call | Why |
|---|---|---|
| 1:1 text, quote-replies, reactions, edit | **KEEP** | Core; each is just a control message referencing a message ID. Edit window ~15 min (matches WhatsApp/iMessage). |
| Disappearing messages (timer on *read*) | **KEEP — default ON** | Best fit: ephemerality reduces data-at-rest, the only place data lives. Signal-grade granularity + a global default-for-new-chats. Tie expiry to mailbox-token lifetime. |
| Voice / video **notes** (async) | **KEEP — flagship** | Map perfectly onto pull-not-push mailboxes; full anonymity + quality (see §11). The on-brand differentiator. |
| Media attachments + file transfer | **KEEP** | Chunked encrypted blobs. **Mandatory client-side EXIF/metadata stripping** in Rust before encryption. |
| On-device search, media gallery, pinning | **KEEP** | Local-only encrypted index; never cloud-indexed. |
| Group chats | **ADAPT** | Capability invite links + per-group rotating mailbox tokens; no global roster (see §3.7). |
| Read receipts | **ADAPT — default OFF** | Leaks presence/sleep patterns. If on: batched/piggybacked into next outgoing message, never instant. |
| View-once media, screenshot warnings | **ADAPT** | Best-effort UI deterrent only — never claim true prevention. |
| Unsend / delete-for-everyone | **ADAPT** | Best-effort "retract" event; **cannot** force-delete a message a peer already pulled. Say so honestly. |
| Link previews | **ADAPT — default OFF** | Receiver fetching a URL leaks their IP. Salvage: **sender** fetches locally, ships a static snippet as an encrypted attachment. |
| Stickers / GIFs | **ADAPT** | Bundled/imported local packs only. **No Giphy/Tenor** (third-party API leaks your search). |
| Live voice/video calls | **ADAPT — opt-in** | Explicit weaker-anonymity mode (see §11). |
| Multi-device | **ADAPT** | Host/controller "dumb terminal" model (see §3.8); not cloud sync. |
| Backup / restore | **ADAPT — default OFF** | Local-only, passphrase-encrypted. Any server/cloud backup silently defeats disappearing messages + E2EE. |
| **Typing indicators** | **DROP** | High-frequency real-time pings = a traffic-correlation signature over Tor; fundamentally anti-pull. |
| **Last-seen / online / presence** | **DROP** | The ultimate metadata leak — intersecting two profiles' "online" times proves they talk. Burn it. |
| **Contact discovery by phone / address book** | **DROP** | No phone numbers exist; address-book upload is mass metadata leakage. |
| **Global username directory / public search** | **DROP** | Creates a lookup graph + harassment surface. Use pairwise QR/invite links instead. |
| **Stories/status, public channels, live location** | **DROP** | Fan-out broadcast + viewer-list metadata chokes a DHT/Tor mailbox model and leaks the social graph. |
| **Cloud backup of plaintext, phone-number identity, FCM/APNs push, in-chat AI** | **DROP** | Each re-introduces a central trust point / compellable third party that breaks the threat model. |

### 12.2 Killer features mainstream apps structurally *cannot* offer
1. **No phone number, no email, no directory** — identity is a keypair; nothing to enumerate, SIM-swap, or subpoena.
2. **Disposable per-relationship identities (burner personas)** — spin up a fresh identity in seconds, talk, delete; the
   network retains zero record it existed. (SimpleX Incognito, taken further.)
3. **One-tap rotating addresses / mailbox tokens** — breaks long-term correlation and spam at the root.
4. **Plausible-deniability vaults + duress passcode** — hidden decoy profiles (own passcode) and a self-destruct passcode
   that silently wipes keys/DB on coercion. (SimpleX has the primitives; we productize it.)
5. **Per-contact compartmentalized routing** — different network posture per contact; compromise of one leaks nothing about
   the others (pairwise-by-design).

### 12.3 Anti-features to deliberately refuse
Phone-number identity · server-side contact sync · cloud/plaintext backups · default-on read/typing/presence telemetry ·
central username search · third-party GIF/link-preview fetch by default · FCM/APNs push · any analytics/telemetry SDK.

### 12.4 Settings spec (privacy-correct defaults; footguns *removed*, not just defaulted)
The hardening reference is **Molly** (the hardened Signal fork). Two rules: (a) make anonymity-critical settings
**non-toggleable** (remove the footgun rather than hope users don't flip it); (b) default everything else to privacy-max and
let users *loosen*, never tighten.

- **Identity & Profiles** — create/rotate/burn identity; **incognito (random name per contact)**; multiple + **hidden decoy
  profiles**; per-contact safety-number/QR verification (alert on key change); export/secure-backup of secret key.
- **Privacy** — Disappearing messages **ON** (global default + per-chat) · Read receipts **OFF** · Typing **absent** ·
  Last-seen **absent** · Link previews **OFF** · View-once + screenshot warning.
- **Security & Lock** — **Database passphrase prompted at setup (mandatory, Argon2id + RAM secret-wipe)** · biometric/PIN
  app-lock + auto-lock on idle/screen-off/network-change · **duress self-destruct passcode** · **Screen Security ON**
  (Android `FLAG_SECURE` truly blocks screenshots+recording; iOS can only blur the app-switcher — *honest platform copy*).
- **Notifications** — Content = **"New message" only** by default (see footgun below) · no sender/preview · per-chat mute ·
  quiet hours.
- **Network & Anonymity** — **Tor always-on (NO off switch)** · `.onion`-required / allow-direct-fallback (warns) ·
  bridges/pluggable transports (obfs4/Snowflake) · **polling interval with jitter** (Stealth/Balanced/Fast/Manual, with a
  battery-vs-latency explainer) · optional Briar-style BLE/Wi-Fi local transport for network blackouts.
- **Data & Storage** — auto-download media **OFF** (tap blurred placeholder to fetch — also blunts zero-click exploits) ·
  auto-delete old media · cache limit · manual **encrypted local-only backup (OFF by default)**.
- **Appearance** — theme; **blur chat-list thumbnails by default**.
- **Advanced** — crypto policy locked to **hybrid X25519+ML-KEM-768 (PQ + PFS always-on, non-toggleable)** · debug logs
  OFF (red warning) · experimental features · "reset anonymity state."
- **Security Checkup screen** (EFF-style) — one place that verifies: passphrase set, notifications content-free, screen
  security on, identity backed up, no untrusted backups.

### 12.5 Real-world footguns to design against
- **The notification cache (the #1 real 2026 footgun, not the crypto).** In a 2025 US case, forensic tools (Cellebrite)
  recovered **expired** Signal disappearing-messages from Apple's persistent **push-notification SQLite cache** — *after*
  uninstall. E2EE and the timer worked; plaintext leaked because the app handed previews to the OS. → **Default
  notifications to content-free "New message"; render text only inside the unlocked app.** Pull-not-push gives us control here.
- **"Bypass Tor for big files/calls"** — the worst footgun; would unmask the IP. **No such toggle.** The anonymity layer is
  the only layer; if Tor is slow, transfers are slow.
- **"Auto-download media"** — a zero-click-exploit vector (Pegasus-style). **No toggle; default OFF.**
- **Custom relay/rendezvous** — a malicious contact could push a relay they log. If allowed, connections to it **must still
  route through Tor.**

### 12.6 New repos/inspiration for features & settings
- **Molly** (`mollyim/mollyim-android`) — DB-at-rest encryption (Argon2id), RAM wipe, UnifiedPush, hardening defaults: the
  settings reference. · **SimpleX** — incognito, hidden profiles, self-destruct passcode, granular `.onion` controls.
- **Threema** — random-ID (no phone) identity model. · **Briar** — Tor-native + offline BLE/Wi-Fi fallback.
- EFF **Surveillance Self-Defense** Signal guide — the canonical "harden these settings" checklist to mirror in the checkup screen.

---

## 13. Security ↔ performance tradeoffs, logs & ephemerality (added 2026-05-30)

A fourth three-way pass, prompted by the product owner wanting **user-chosen speed/anonymity tradeoffs** (per contact, for
messages *and* calls), a **normal-phone call/message log**, and **view-once messages**. Codex and Gemini split hard on the
first one; **real-world evidence (SimpleX) settles it.**

### 13.1 "Let users pick Tor vs direct per contact" — the honest answer is: bind it to an *identity*, not a contact
Your instinct (fast direct for people you trust, slow-anonymous for the rest) is reasonable — but a **per-contact** toggle
is the one place the whole field says *don't*. Why:
- **Cross-contamination (Gemini's key point):** if your *one* identity runs Tor for Bob **and** direct for Charlie, you've
  now (a) handed your real IP to Charlie, and (b) made your "anonymous" Bob traffic *easier* to de-anonymize, because the
  same device now exposes a correlatable Tor-plus-clearnet pattern. Anonymity is a property of the **device/identity's
  whole network behaviour**, not of a single chat.
- **Weakest-link:** transport privacy is bounded by the *least-protected* participant. A contact who picks "direct" exposes
  the **relationship** — deanonymizing the *careful* party on the other side too.
- **Anonymity-set partition + fingerprint:** if it's a choice, most pick "fast," so the few on Tor stand out; and the
  research shows per-contact Tor **stream isolation** spins up many circuits = a distinctive fingerprint + smaller
  anonymity set. Over-isolation is *itself* a signal.
- **Precedent is unanimous:** SimpleX buries per-contact transport isolation behind **developer tools, labelled
  experimental** (it breaks with large groups); Briar and Session expose **no** per-conversation transport dial. The field
  picks **one uniform strong transport** and makes it the default.

**So — the safe design that still gives you what you want:**
1. **Transport is bound to an IDENTITY/PROFILE, not a contact.** Run two profiles:
   - **Anonymous profile** → always Tor/oblivious. For sources, sensitive contacts.
   - **Personal/Direct profile** → may use direct P2P (fast calls/video), fully E2E-encrypted but **not** network-anonymous —
     for people who already know who you are IRL. Assign each contact to a profile; the app keeps them isolated.
   This delivers fast direct calls with IRL friends **without** poisoning your anonymous identity — because the direct-using
   identity never also runs Tor-for-anonymity.
2. **Async messages: always Tor, in every profile.** There is *no* latency reason to expose your IP for a text. Direct is
   only ever relevant for **live calls / large media**.
3. **If a direct path is ever negotiated, copy SimpleX's downgrade UX:** an explicit per-message **orange "IP exposed to
   contact" warning**, with a cautious default (`Allow downgrade: only when IP stays hidden / never`). Never a silent
   downgrade, never an auto-"fall back to direct if Tor is slow."
4. **High-threat guidance:** for truly sensitive use, use a dedicated device/identity and never enable direct on it.

> Net: **yes you can have the fast direct option** — but as a *separate identity you opt into*, not a per-contact switch on
> your anonymous one. (This refines the earlier "one uniform transport" rule: uniform **per identity**.)

### 13.2 Which "lower security for performance" knobs to expose vs remove
Strong consensus (Codex + Gemini + research):

| Knob | Expose? | Why / default |
|---|---|---|
| **Polling / sync frequency** | ✅ Expose | Pure battery↔latency, no security impact. Presets: Battery / Standard / Low-latency. Default **Standard**. |
| **Media auto-download** | ✅ Expose | Default **Wi-Fi-only + size cap** (also blunts zero-click media exploits). |
| **Profile transport (Anon vs Direct-capable)** | ✅ Expose (per *identity*) | Per §13.1. Default = Anonymous/Tor. |
| Cover traffic / padding level | ❌ Remove | Must be **uniform** — if only the cautious enable it, cover traffic *becomes* the dissident signature. Tie to profile, not a dial. |
| Latency / jitter manual tuning | ❌ Remove | Auto-tuned internally. |
| Tor hop count / circuit length | ❌ Remove | Classic footgun; users can't evaluate the entropy tradeoff. |
| "Force direct if Tor slow" / SMS fallback | ❌ **Never ship** | Silent deanonymization. **Fail closed**, not open. |

### 13.3 Call & message logs ("recents" like a normal phone) — yes, but local-only
You can have the familiar recents list (last-spoken, **call duration**, message date/time, missed/in/out). The rule:
**it is local-only metadata and must never touch the network** (presence is never published — that's the §3.6 pull-not-push
guarantee). Handling:
- **Store** (in the encrypted-at-rest DB): contact pseudonym, direction, call start/end/duration, message timestamps,
  unread counts, last-activity.
- **Never store:** IPs, relay/circuit IDs, transport-path history, or precise failure reasons (those reconstruct network
  state).
- **Retention control:** "Keep history: Off (volatile, wiped on lock) / 30 days / 90 days / Forever." Default to a short
  window. Gemini's stricter view (keep call history in RAM, wipe on background) becomes the **Off** option for high-threat users.
- **Duress wipe** must clear the DB keys **and** the notification-cache mirror, thumbnails, waveform previews, and OS recents/
  widgets.
- **Cellebrite lesson (again):** redacted "New message" notifications by default; decrypt only in-memory after unlock. A
  persistent on-disk social-graph + routine log is exactly what device seizure wants — that's why retention defaults short
  and the duress wipe is thorough.

### 13.4 View-once / one-time-read — ship it, but call it what it is
You already spotted the flaw (screenshots / a second camera). The research is blunt: **this category only ever stops
*casual* capture, never a motivated recipient.** Specifically:
- It's **client-enforced cooperation**, not crypto. WhatsApp's "View Once" was trivially bypassed on Web (Sept 2024) and
  they had to bolt on **server-side** enforcement — which a **serverless** app like ours *cannot* do. So for us it's a pure
  best-effort hint between honest clients.
- **Android `FLAG_SECURE`** genuinely blocks on-device screenshots, screen-recording, the app-switcher thumbnail, and
  Microsoft-Recall-style capture — cheap and worth doing. **iOS has no equivalent**: it can only *detect after the fact*
  (`userDidTakeScreenshotNotification`, `isCaptured`), never prevent. **Nothing** stops a second phone photographing the screen.
- Screenshot *detection* ("they screenshotted") is unreliable (Telegram admits it) and, in a P2P app, depends on the
  hostile recipient's client honestly self-reporting — so **don't ship it as a real feature** (or label it explicitly
  best-effort).

**Therefore:** ship it as **"Self-destructing / Ephemeral"** (never "secure" or "can't be saved"), media-first, with a
**persistent, honest warning**: *"Best-effort. Anyone can photograph their screen with another device. Don't rely on this
for anything you couldn't bear to be saved."* Sender sees **Opened / Expired-unopened**. Its real, honest value: prevents
*accidental* resurfacing and reduces data-at-rest for a *future* device seizure. Close the side channels we **do** control —
no notification/preview/thumbnail/backup leakage; wipe decrypted bytes from memory after viewing; decrypt only at view time.

### 13.5 The single most dangerous setting we could ship
Both advisors converge: **cloud backup / "link device" that exports keys or history off the phone.** It hands ciphertext +
metadata to a compellable tech giant or a weaker desktop sandbox. **Neutralize by removing it:** no cloud backup ever; keys
stay in the device keystore/secure element; multi-device only via the host/controller proxy (§3.8), never by copying the
primary private keys.

---

## 14. Tech stack & cross-platform architecture (added 2026-05-30)

Reconciled from a fifth three-way pass. **Strong consensus** (Codex + Gemini + research): the only proven model for a
security-critical messenger across iOS/Android/Windows/Linux is **one Rust core + native UIs** — the **Signal/Molly**
architecture. The research found *every* leading secure messenger separates a single shared core from per-platform native
UIs; nobody ships the secure surface in a cross-platform UI framework. Core-language momentum is clearly **Rust** (Signal,
Session-leaning) — it's the only core that is natively compilable to all four targets, memory-safe, and has first-class FFI
to Swift/Kotlin/Node. Decisively, **`arti` (Tor) is itself a Rust crate**, so transport composes into the same core with zero
cross-language plumbing.

### 14.1 The architecture
```
Rust core (one workspace)                         exposed via UniFFI (narrow JSON command/event API)
  crypto (X25519+ML-KEM-768, ratchet, ML-DSA)        │
  arti Tor transport + oblivious-mailbox/pull        ├──► Swift bindings  → iOS UI: SwiftUI
  session/group/device state machines                ├──► Kotlin bindings → Android UI: Jetpack Compose
  SQLCipher-encrypted local storage                  └──► in-process      → Desktop: Tauri 2 (Win/Linux/macOS)
```
- **FFI = UniFFI** (Mozilla, v0.31, powers Firefox app-services) — auto-generates Swift+Kotlin bindings, sparing Signal's
  hand-written JNI/Swift maintenance. The single biggest small-team multiplier. Keep the FFI boundary **thin and versioned**
  (UniFFI is pre-1.0; most logic stays in Rust, bindings regenerate). Copy SimpleX's `chat_send_cmd` / `chat_recv_msg` async
  JSON pattern — platform-agnostic and testable.
- **Mobile UI = native** (SwiftUI / Jetpack Compose) for best voice/video, background-pull, push-less delivery, and keystore
  use. **Desktop = Tauri 2** (Rust core in-process, web UI; ~3 MB vs Electron ~96 MB; passed a Radically Open Security audit).
- **Crates:** `tokio`, `arti-client`, `serde`+`postcard`, `x25519-dalek`/`ed25519-dalek`, a pinned-and-audited `ml-kem` +
  `ml-dsa`, `chacha20poly1305`, `blake3`/`sha2`/`hkdf`, `sqlcipher`, `zeroize`/`secrecy`.

### 14.2 The one advisor disagreement — does Tor/sockets go *in* the Rust core?
- **Codex + research:** yes — `arti` is Rust, embed it in the core (one FFI for crypto *and* transport).
- **Gemini:** no — keep Rust for "computing bytes"; let Kotlin/Swift own sockets/lifecycle/filesystem, because iOS/Android
  power-management and background-socket policies differ wildly and a Rust daemon owning sockets gets **silently killed**.
- **Our resolution:** **`arti` lives in the Rust core (bytes + Tor logic), but the NATIVE layer owns lifecycle/scheduling/
  power** — Android foreground service / WorkManager decides *when* to wake and sync; iOS BGTask/relay-wake does. Rust never
  tries to be an always-on daemon. This captures the efficiency of embedded arti **and** Gemini's correct warning.

### 14.3 Platform reality (honest answer to "Apple, Android, Windows, Linux")
- **Android ✅** — persistent foreground service over Tor (~few % battery/day, survives reboot); true serverless pull works.
- **Desktop (Win/Linux/macOS) ✅** — real background daemons allowed; Tauri 2.
- **iOS ⚠️** — *the hard case.* No background Tor/sockets; PushKit is call-only (iOS 18 stricter); the **only** scalable wake
  is **APNs**, which needs a server with your device token. **Reference workaround (SimpleX): an oblivious APNs notification
  relay** that sends an encrypted metadata *envelope* (server correlates nothing inbound↔outbound); the app wakes, decrypts,
  pulls. So even a "serverless" product needs a **small oblivious push relay for iOS** — scope/fund/threat-model it as core,
  not a bolt-on. **Recommendation: Android + Desktop for v1; iOS (foreground-only, or with the relay) as v2.**

### 14.4 Build / CI / distribution
- **CI:** GitHub Actions (macOS runners for iOS notarization; Linux for Rust). Locked toolchains (`rust-toolchain.toml`,
  Gradle/SwiftPM pins, Nix/containers). `cargo-ndk` for Android `.so`, xcframework for iOS. Supply chain: `cargo vet` +
  `cargo audit` + SBOM/SLSA provenance.
- **Reproducible builds** — first-class CI requirement (F-Droid now shows per-app ✔️/💔 reproducibility); a primary trust
  signal, hard to retrofit — bake in from day one.
- ⏰ **Distribution bombshell — Google Developer Verification (from Sept 2026):** *every* Android app on a certified device —
  even sideloaded/F-Droid — must come from a developer who registered a **real legal identity** with Google; an on-device
  verifier blocks unregistered apps by default. **F-Droid has said it "would cease to function" as written.** → pvtcoms
  **cannot be published anonymously on Android**: register a **named legal entity/foundation** as the developer while keeping
  *users* anonymous; assume F-Droid may break; keep a self-signed-APK sideload path. (No-phone-number *onboarding* itself is
  store-permitted — Session/Threema/Briar prove it; the friction is *publisher* anonymity + Apple privacy-manifest compliance,
  which rejected ~12% of Q1-2025 submissions.)

## 15. Gaps & additional workstreams (added 2026-05-30)

Both advisors stressed: the cryptography is the *easy* part; these under-specified areas are where projects like this die.
Now tracked (summarized in README §"Gaps"):

1. **Formal threat-model document** — assets, adversary capabilities (device seizure/Cellebrite, 0-click/NSO, global passive),
   and an explicit **out-of-scope** list. A maintained artifact. **(§16 provides the substance to seed this: the onboarding
   in/out-of-scope split, the open-source posture, and the endpoint trust boundary.)**
2. **Onboarding / first-run** — Tor cold-start (5–30 s) UX; the "first 60 seconds" is currently undesigned and a churn risk.
3. **Identity & device recovery** — key loss = identity loss. Define BIP39/encrypted-backup recovery + irreversible-loss UX +
   low-trust device migration.
4. **"Dead token" rediscovery** (Gemini) — a user offline past the token-rotation window goes undiscoverable; contacts must
   re-establish without an out-of-band anonymity leak. Needs a protocol-level answer (e.g. overlapping long-lived fallback
   rendezvous derived from the master secret).
5. **State/UI consistency under latency** (Gemini: most under-thought) — delivery-state semantics (Sent / In-mailbox / Pulled /
   dropped) and concurrent edits to un-pulled messages, with no server to arbitrate order.
6. **Distribution playbook** — see §14.4 (Google verification, F-Droid risk, named-entity publisher, the iOS push relay).
7. **Reproducible builds** — §14.4.
8. **Named third-party audit** (Quarkslab/Cure53/Trail of Bits/NCC) of PQ crypto + Tor + mailbox protocol **before** any
   "secure" marketing — a funded **release gate**, not a stretch goal.
9. **Abuse handling in E2EE** — message/transcript **franking** so a recipient can report specific harmful content to a
   moderator without the platform reading traffic. Design in from day one.
10. **Telemetry-free crash reporting** — opt-in, user-reviewable, scrubbed local dumps; no off-the-shelf privacy-respecting
    pipeline exists, so build it.
11. **Accessibility (screen readers/dynamic type/keyboard), i18n+RTL, secure update channel + rollback protection,
    crypto-agility/PQ-version negotiation policy, legal/export-control posture, and funding/sustainability + maintainer
    bus-factor.**

---

## 16. Onboarding security, open-source posture & endpoint threat model (added 2026-05-30)

A sixth three-way pass (Claude research + Codex + Gemini), answering three of the product owner's questions. The advisors
**fully agreed**; the framing principle (Gemini) worth keeping front-of-mind:

> **Authentication is harder than encryption.** Establishing *who* you're talking to over a distance is the hardest problem
> in secure comms. A weak key exchange makes your post-quantum encryption "a perfectly secure tunnel to an imposter."

### 16.1 Adding a contact safely (the "QR/sharecode over WhatsApp" question)
**First, the reassuring part: a correctly-designed invite NEVER contains your private keys.** It carries only a *rendezvous
address* + an *ephemeral public* key (to bootstrap the exchange). So Meta/WhatsApp seeing the invite **cannot** decrypt your
pvtcoms messages — there's nothing private in it. The user's fear ("they get hold of my keys") doesn't apply if the format is
right.

What an attacker on the insecure channel *can* actually do — three real risks:
1. **Invite theft / first-connect impersonation** — someone grabs the link and connects *before* your friend, so "Alice" is
   actually the thief.
2. **Active MITM / key substitution** — attacker swaps the key in transit and relays between you both.
3. **Metadata** — the channel provider learns you two are setting up a pvtcoms connection.

**The defenses (all proven in the field, mapped to our mailbox model):**
- **Single-use, self-detecting invite links (SimpleX v6.4 pattern).** The invite is one-time: the mailbox queue is *locked*
  to the first redeemer via a signature key. So **link theft becomes a visible failure** — if a thief redeems it, your real
  friend's connection *fails loudly* and you both know to regenerate. This converts a silent compromise into an alarm. (We
  can deliver this because we own the mailbox semantics.)
- **Cryptographically bind the invite** (PQ hash + hybrid signature over rendezvous address + token seed + ephemeral key) so
  no relay/mailbox can substitute a different key — the client rejects any mismatch.
- **Post-connect SAS / safety-number verification over a SECOND channel** — the only thing that actually *detects* a
  first-contact MITM. Derive a per-pair fingerprint from both long-term identity keys; compare a short word-list **over a
  different channel you trust** (a voice/video call where you *recognise the person*, or in person). Use a **ZRTP-style
  hash-commitment-then-reveal** handshake so the string can be short (≈a few words) yet still bind an active attacker to a
  single ~1/65536 guess.
- **Threema-style visible trust levels** (not a hidden boolean): default **"unverified — key from rendezvous, could be
  MITM'd"**; only promote to **"verified"** after SAS/QR. Plus **Signal-style key-change alerts**: if a verified contact's key
  ever changes, **block sending until re-verified** (honest copy that reinstall/new-device are benign causes).
- **Briar-style introduction-by-mutual-contact** — an already-verified contact vouches for and bridges two peers. The safest
  *remote* path when you can't meet in person (high value for an anonymous P2P app).

**Concrete safe recipe:** Alice makes a one-time invite (15-min expiry) → sends it over WhatsApp → Bob redeems (queue locks)
→ both see a 6–8 word SAS → they compare it on a voice call / in person → app marks **verified** and pins the key → any later
key change interrupts. **When is WhatsApp-sharing fine?** As a *rendezvous bootstrap that you then confirm by SAS* — fine for
ordinary contacts; **dangerous** if treated as final trust (journalist↔source must do in-person QR or SAS-over-recognised-voice).

**In scope (we solve):** passive link theft (made self-detecting), relay/server key substitution (prevented by signed
invites), active first-contact MITM (*detectable* via mandatory out-of-band SAS). **Out of scope (crypto can't):** proving the
*human* who received the link is the intended person — only the user doing the SAS check closes that; and the security of the
delivery channel itself. **Default the UI to "rendezvous established, NOT yet verified" after any link-over-insecure-channel.**

### 16.2 "If someone gets the source code, is it still safe?" — Yes. That's the point.
This is **Kerckhoffs's principle / Shannon's maxim**: *the system must be secure even if everything except the key is
public.* Security lives in the **secret keys on your device**, never in hiding the code. Publishing the source is a **net
security benefit** — it enables independent audit, reproducible builds (proving the binary matches the audited source), and
academic scrutiny. **Closed/secret crypto has a disastrous track record** (homerolled primitives, nonce misuse, timing leaks).
- **Must stay secret:** private identity keys, ratchet state, recovery secrets, device-unlock factors.
- **Fine to publish:** protocol specs, crypto choices, all source, build scripts, threat-model docs.
- **Honest caveat:** open source ≠ automatically secure — attackers read it too, "many eyes" is weak for PQ crypto (few
  qualified auditors), and supply-chain attacks happen (the 2024 `xz` backdoor). So open source is a *prerequisite for trust*,
  not a guarantee — pair it with a funded audit, fuzzing, `cargo vet/audit`, and fast patching. **If publishing the code would
  make pvtcoms unsafe, it was relying on obscurity, not cryptography.**

### 16.3 "Can other apps on the device listen?" — the endpoint threat
The hard truth: **cryptography protects data in transit and at rest, but NOT data in use.** On the screen / in memory, the
*device* is the final authority — if the OS is compromised, the app is compromised. Platform strength ranks **iOS > Android ≫
desktop**.

**In scope — pvtcoms WILL do (and a normal malicious app can't beat these on an intact OS):**
- App sandbox reliance (per-app UID/container) + **hardware-backed non-exportable keys** (iOS Secure Enclave / Android
  Keystore, **StrongBox** where available).
- **SQLCipher (AES-256) at-rest DB**, key wrapped by Keychain/Keystore, iOS `NSFileProtectionComplete` (unreadable while
  locked). Defeats other apps **and** offline/seized-disk extraction.
- **`FLAG_SECURE` on all Android windows** + the **desktop screen-capture/DRM flag** (the Signal-on-Windows trick) to defeat
  **Microsoft Recall**-class screen scrapers.
- **Clipboard hygiene** — never put secrets/codes/recovery phrases on the clipboard without auto-clear; mark sensitive
  (`EXTRA_IS_SENSITIVE`); prefer QR/scan over paste.
- **Memory hygiene** — keep keys in the Rust core, `zeroize` buffers, short in-RAM key lifetimes, no secret-laden crash dumps.
- **Defense-in-depth:** detect+warn on rooted/jailbroken devices and on active accessibility services / screen recording;
  minimize on-device data (disappearing messages default) so the post-compromise window is small; tiny binary + fast updates.

**Out of scope — pvtcoms honestly CANNOT defend (must be stated plainly in the threat model):**
- A **compromised/rooted OS or 0-click mercenary spyware** — **Paragon "Graphite" (2025) compromised fully-patched iPhones**
  (CVE-2025-43200); **Pegasus/NSO** likewise. Once the OS is owned, the attacker reads plaintext, keys, screen, keystrokes —
  sandbox, Keystore, SQLCipher, FLAG_SECURE are all moot. This is an OS-vendor responsibility.
- A **user-granted malicious accessibility service or full-access keyboard** — **Sturnus malware (Nov 2025) reads Signal /
  WhatsApp / Telegram plaintext in real time** *post-decryption* by walking the UI tree; it even bypasses FLAG_SECURE via the
  accessibility path. We can warn, not block, what the user authorised.
- **Physical access to an unlocked device**, shoulder-surfing, and the **second-camera** capture of the screen.
- **Desktop is structurally weakest** — no per-app sandbox; any user process can screenshot/keylog/read files (Recall is the
  poster child). Treat desktop as a **degraded-trust companion**; default screen-protection on; steer high-risk users to mobile.

**Posture:** don't chase the un-winnable (endpoint hardening against a compromised OS is the OS vendor's job) — instead **do
the in-scope hygiene well, document the boundary honestly, and recommend GrapheneOS on a dedicated Pixel** (Storage/Contact
Scopes, sandboxed Play, per-app network/sensor toggles) as the "maximum endpoint" tier for high-threat users.

> **Threat-model sentence (seeds §15.1):** *pvtcoms protects the network/transport (serverless P2P, Tor, post-quantum E2EE,
> oblivious mailboxes) and on-device data against other normal apps and offline/seized-disk extraction — provided the OS is
> intact and the device is locked. It does NOT protect plaintext endpoints against a compromised OS or user-granted spyware;
> that is the device/OS-vendor trust boundary.* The single most dangerous misconception is that "post-quantum + Tor" makes you
> safe on a backdoored phone or with careless first-contact verification. It does not.

---

### Appendix: where Codex and Gemini disagreed (and our call)

| Thread | Codex | Gemini | Our verdict |
|---|---|---|---|
| Groups | Sender-Keys first, then migrate to MLS (RFC 9750 enables decentralized DS) | MLS is a serverless fantasy; stay on Sender-Keys for < 50 | **Sender-Keys v1, hub-mediated OpenMLS v2, DMLS as research** — both are right at different scales |
| Multi-device | Device-tree: master key + per-device keys, authorization ceremonies | Don't sync; desktop = dumb terminal driven by phone | **Dumb-terminal v1 (ship now), device-log v2 (Codex's model, once proven)** |
| Anti-spam | Hybrid PoW + Privacy Pass + RLN | Rotating mailboxes already solve spam; global anti-spam is overengineered | **Gemini's web-of-trust + PoW gate; defer RLN** |
| Transport modes | Offer fast/balanced/slow with a safe default | A footgun — partitions the anonymity set | **Gemini: one uniform transport; vary only UX/battery mode** |
| Live-call default | Blind relay-only TURN (Signal-style, hides IP from peer) | No relays at all — direct clearnet P2P + scary warning, or it fails (don't become a metadata broker) | **Synthesis: BYO/self-hosted relays; expose "Direct" vs "Relayed" per call; pvtcoms operates none. Rui to pick default** (async voice notes are the real privacy answer) |
| Tor-vs-direct user choice | Per-contact connection profiles (Private/Balanced/Fast, "weakest peer" badge) | Catastrophic per-contact; transport is a per-device property — use separate isolated *profiles* | **Gemini, confirmed by SimpleX precedent: bind transport to an IDENTITY/profile, not a contact. Anonymous profile (Tor) + opt-in Direct-capable profile. Async always Tor. Per-message downgrade warning if direct negotiated** (§13.1) |
| Tor/sockets in the Rust core? | Yes — `arti` is Rust, embed transport in the core | No — Rust computes bytes; native owns sockets/lifecycle or the OS silently kills it | **Both: `arti` in the Rust core, but native owns lifecycle/scheduling/power; Rust is never an always-on daemon** (§14.2) |
| iOS in scope for v1 | Native SwiftUI, accept foreground-only pull | "Abandon iOS for v1" — Apple makes background Tor impossible | **Android + Desktop v1; iOS v2 via oblivious APNs relay (SimpleX pattern). Rui to confirm** (§14.3) |
