---
last_verified: 2026-05-30
verified_version: 0.1.48
owner: backend
freshness_days: 30
---

# Spec — Invite Sharing & QR Feature

How a user shares an invite and adds/verifies a contact. Builds on `DESIGN.md` §16 (onboarding) and `THREAT_MODEL.md`
(A5/A6, O4). Reconciled from Codex + Gemini + research (SimpleX v6.4, Signal, Briar, Threema).

## Principle
**Adding ≠ verifying.** Sharing a link only reaches *connected, unverified*. Trust requires an out-of-band **SAS** check.
*Authentication is harder than encryption.*

## Three parallel transports, one token
Field consensus (SimpleX/Signal/Briar): offer all three for the *same* underlying single-use token — don't force one.
1. **QR** — for in-person (the only path that may auto-elevate to *verified*).
2. **Short copyable string** — paste anywhere.
3. **OS share sheet** — Android `ACTION_SEND` / Tauri desktop share — to send remotely (e.g. via WhatsApp), with a caution.

## Invite format
- **Secret lives in the URL fragment** (`#...`) so it is never sent to any server/relay or written to a request log
  (SimpleX's short-link trick, adapted to serverless). Fragment carries: oblivious-mailbox rendezvous locator + **hybrid
  X25519 + ML-KEM-768 KEM material** (so the *first* message is post-quantum — a high-threat user may send only one) + the
  SAS seed + expiry + an integrity signature.
- **Android**: ship **verified HTTPS App Links** (`autoVerify="true"` + pinned `/.well-known/assetlinks.json` = package +
  SHA-256 cert) — **not** a raw `pvtcoms://` custom scheme, which any malicious app can register and hijack. Desktop:
  custom scheme via `tauri-plugin-deep-link`. **Validate/allowlist every parsed field in the Rust core** (shared by both clients).
- Generate QR + render in the **shared Rust core** (`qrcodegen`) so Android and Tauri show identical codes; scan with
  CameraX + ML Kit / ZXing.

## Lifecycle (single-use + time-boxed)
`created → (shared) → pending → redeemed(once) → connected/unverified → [SAS] → verified`
- **Burn on first use** (self-detecting theft: a thief redeeming it makes the intended peer's redemption fail loudly).
- **TTL 24–48h** (Briar-style). Explicit **pending / connecting-via-Tor / failed-after-timeout / expired** states — never a silent failure or fake spinner.

## Verification (SAS)
- After connect, both sides see a short **transcript-bound** SAS (emoji or BIP39-style words), rendered identically from the
  core, compared **out-of-band** (voice call where you recognise the person, or in person). MITM alters the transcript → SAS differs.
- **In-person QR scan** is the only flow that auto-marks *verified*; a forwarded link can only reach *unverified* + a SAS prompt.
- Threema's rule, surfaced in-product: *"verification codes are scanned from the person's phone, never sent over the internet."*

## Anti-abuse UX (quishing / screenshot leakage)
- QR phishing ("quishing") is ~12% of phishing payloads (2025); ~73% scan without checking. Defence is the **human SAS step**, not the transport.
- Warn that a **screenshot of a QR** can sync to Google Photos/iCloud and leak the one-time secret — prefer live scan; mark invite media sensitive; don't auto-screenshot.
- When an invite arrives via a *forwarded* link, show a caution and **require SAS before any trust**.
- *(Optional, advanced — not v1)* Gemini's harder variants: animated multi-frame QR (defeats single screenshot) and
  "active SAS" (Bob picks the 3 words Alice reads from a list of 9). Track as future hardening; v1 uses standard SAS + advisory model.

## Crates / APIs
`qrcodegen` (core), CameraX+ML Kit / ZXing (Android scan), `tauri-plugin-deep-link` (desktop), Android App Links +
`assetlinks.json`, `ACTION_SEND` share sheet. SAS = truncated transcript hash → emoji/word list.

## Tracking
Implementation: see `BACKLOG.md`. Tests: invite parse fuzzing, single-use/expiry state tests, MITM-onboarding + stolen-token negatives, deep-link allowlist validation (`docs/TEST_PLAN.md`).
