---
title: pvtcoms — Security Audit Scope
status: pre-audit (NOT yet audited)
last_verified: 2026-06-12
verified_version: 0.1.48
---

# pvtcoms — Security Audit Scope

This is the **scoping package** for an external security audit of pvtcoms. It is written for an
auditor/firm to estimate effort and conduct a review. **pvtcoms has NOT been audited.** No "secure"
claim should be made publicly until an independent audit is complete and findings are remediated.

> **Audited target:** the engagement reviews the annotated tag **`audit-0.1.40`** (an audit quotes
> against a frozen tag, not a moving version). This package is current as of **v0.1.40** (a docs-only
> refresh; the last *code* change was v0.1.39, commit `e85f05f`, so the tag carries the latest scoping
> docs alongside the exact reviewed code). If more commits land before the engagement starts, re-tag.
> Repo is AGPL-3.0, full source provided.

> **What changed since this package was first drafted (v0.1.8 → v0.1.39).** The crypto/protocol core is
> unchanged; the deltas are transport hardening, storage relocation, an operability layer, two
> newly-wired privacy primitives, and a crate rename. See §11 for the full list — an auditor returning
> to the package should read that section first.

Companion documents in this folder:
- [`CRYPTO_SPEC.md`](./CRYPTO_SPEC.md) — precise cryptographic design (for the crypto reviewer).
- [`SECURITY_CLAIMS_AND_LIMITATIONS.md`](./SECURITY_CLAIMS_AND_LIMITATIONS.md) — falsifiable claims to
  validate/refute, self-identified weaknesses, prior findings + fixes, and focus questions.
- Plus the maintained project artifacts: [`../../THREAT_MODEL.md`](../../THREAT_MODEL.md),
  `DESIGN.md` (internal full-design doc, shared with the auditor), [`../ARCHITECTURE.md`](../ARCHITECTURE.md),
  [`../../STACK.md`](../../STACK.md), the ADRs in [`../ADR/`](../ADR/), and
  [`../reports/2026-06-02-aead-nonce-audit.md`](../reports/2026-06-02-aead-nonce-audit.md).

---

## 1. What pvtcoms is

A serverless, peer-to-peer, **anonymous, post-quantum** encrypted 1:1 text messenger. No phone number,
no account, no operator-run server that can read messages or metadata. Transport is **Tor v3 onion
services** (anonymity, IP-independence); a volunteer/self-hostable **oblivious relay** provides offline
store-and-forward without learning who talks to whom. End-to-end crypto is a **hybrid classical+PQ**
handshake (X25519 + ML-KEM-768) feeding a **Double Ratchet**.

It is **pre-1.0** (v0.1.40; last code change v0.1.39). The desktop app (Rust core + local browser UI + system tray) is functional;
mobile apps are at the binding-layer stage (UniFFI). First-contact identity is **trust-on-first-use**
with an out-of-band safety number for verification.

## 2. Audit goals

1. **Cryptographic review** of the hybrid PQ handshake, the Double Ratchet, the offline send-chain +
   one-time-prekey (PCS) scheme, AEAD nonce management, at-rest sealing, and key derivation/binding.
2. **Protocol/metadata review** of the oblivious relay, rotating addressing tokens, directory records,
   invite/onboarding, and what the relay/network observer can learn (linkability, traffic analysis).
3. **Implementation review** (Rust): memory/panic safety on attacker-controlled input, the hand-written
   wire parsers, constant-time comparisons, key zeroization, the FFI boundary.
4. **At-rest & keystore review**: SQLCipher usage, the per-platform device-key storage (Windows DPAPI,
   macOS Keychain, Linux Secret Service), and the data-safety/migration logic.
5. **Local attack surface** of the desktop app (localhost HTTP UI, CSP, CSRF guard, the browser
   front-end rendering untrusted message content).

## 3. In scope

| Component | Crate / path | ~LOC | Notes |
|---|---|---:|---|
| Crypto primitives + KDF + AEAD + safety number | `core/src/crypto.rs` | 327 | wraps audited crates; `seal`/`open`, `seal_at_rest`/`open_at_rest`, `hybrid_session_key`, `safety_emoji` |
| Hybrid PQ KEM facade | `core/src/pqkem.rs` | 181 | libcrux-ml-kem (primary) + ml-kem differential KAT |
| Authenticated handshake | `core/src/handshake.rs` | 460 | X25519+ML-KEM-768, identity-signed transcript |
| Double Ratchet | `core/src/ratchet.rs` | 436 | symmetric HMAC chain + X25519 DH ratchet, skipped keys |
| Wire framing + parsers | `core/src/wire.rs` | 281 | length/version-bounded; **untrusted network bytes** |
| Identity / contacts / rotation | `core/src/{identity,contacts,rotation}.rs` | 839 | Ed25519, pinning (TOFU→verified), signed key migration |
| Offline messaging | `core/src/{sendchain,prekey,offline,outbox,media}.rs` | 2296 | FS send-chain, one-time hybrid prekeys (PCS), R-derived envelope AEAD, outbox, media lane |
| Oblivious relay + addressing + anti-abuse | `core/src/{relay,directory,mailbox,pow}.rs` | 1961 | rotating tokens, blinded records, hashcash PoW, relay `stats()` for the health endpoint |
| Traffic-analysis: poll jitter + cover deposits + padding | `core/src/{cover,pad}.rs` | 389 | jittered poll/cover cadence, `cover_blob`, `self_deposit_token`, bucket padding |
| Invite / onboarding | `core/src/invite.rs` | 576 | X3DH-style async first contact |
| App: chat engine, transport, storage, keystore, data dir | `client/src/{chat,mailbox,offline_client,tor,histdb,oskeystore,paths}.rs` | ~5550 | Tor via arti, fail-closed routing, SQLCipher history, DPAPI/keyring at-rest key, app-data-dir + migration |
| App: directory / invite / rotation clients | `client/src/{dir_client,invite_client,rotate_client}.rs` | 759 | publish/lookup-by-identity (auto-publish on online), invite + rotation flows |
| Local UI server + front-end | `client/src/gui.rs` + `client/src/gui.html` | 1349 + html | localhost axum API, CSP, CSRF guard, plain-text message rendering |
| Native FFI surface | `ffi/src/lib.rs` | 210 | UniFFI; opaque `Identity`, seal/open, safety number |
| Concurrency (loom-checked lock registry) | `concurrency/src/lib.rs` | 136 | per-contact lock interning, model-checked |

Total ≈ **15,900 LOC of first-party Rust** + ~1,200 lines of front-end JS/HTML. (The crate previously
named `demo/` is now `client/` — same shipped desktop app, see §11.)

**Bundled native libraries** (statically linked, version-pinned — review their *integration*, not their
internals): SQLCipher (SQLite **3.50.4**) + OpenSSL **3.6.2** (DB AES); `ring` (rustls). Floors are
CI-enforced (`scripts/ci/check_dep_floors.py`).

## 4. Out of scope (this engagement)

- The internals of audited upstream crates (see §6) and bundled C libs (OpenSSL/SQLite) — pinned +
  monitored, not re-audited here.
- The Tor network itself / `arti` internals (we depend on it; its anonymity properties are assumed).
- Items the threat model explicitly does **not** defend (see [`../../THREAT_MODEL.md`](../../THREAT_MODEL.md)
  §4): a fully compromised endpoint, global passive adversary correlation, the user's own OS/keyboard,
  coercion. The auditor should confirm we don't *claim* protection we can't deliver.
- iOS/Android app **shells** (not yet built — only the UniFFI binding layer exists).

## 5. Trust boundaries & data flow

```
  Alice's device                          (untrusted)                     Bob's device
 ┌───────────────────────────┐        ┌──────────────────┐        ┌───────────────────────────┐
 │ UI (browser, localhost)   │        │  OBLIVIOUS RELAY │        │  UI (browser, localhost)  │
 │   │  ▲  CSP + CSRF guard   │        │  (volunteer/self │        │                           │
 │   ▼  │  plain-text render  │        │   -hosted; sees  │        │                           │
 │ Rust core ── encrypt ──────┼──TLS?  │  only opaque,    │  ──────┼─► Rust core ── decrypt    │
 │  handshake→ratchet         │  NO →  │  rotating tokens │        │   verify identity (pin)   │
 │  ── seal_at_rest ──┐       │ all    │  + padded blobs; │        │                           │
 │                    ▼       │ over   │  burns on read)  │        │                           │
 │ SQLCipher DB + OS keystore │ TOR ───┼──────────────────┼── TOR ─┤ SQLCipher DB + OS keystore│
 └───────────── boundary S ───┘ (v3    └──── boundary M ──┘  onion └─────────── boundary S ────┘
                                onion)                        svc
        boundary L (same-machine: UI↔core)        boundary FFI (mobile: secrets stay in Rust core)
```
Trust boundaries: **M** network/relay (must learn nothing linkable); **S** at-rest (sealed under the
device key, itself in the OS keystore); **L** local UI↔core (localhost HTTP; CSP + same-origin/CSRF
guard; plain-text render); **FFI** Rust↔native-app (the secret key never crosses). The relay/network is
reached **only over Tor** — there is no clearnet path (fail-closed).

- **Assets:** message plaintext, long-term Ed25519 identity secret, ratchet/session keys, the device
  storage key, contact graph, and **metadata** (who-talks-to-whom, when). See THREAT_MODEL §1.

## 6. Audited / trusted dependencies (exact pins in `Cargo.lock`)

x25519-dalek 2.0.1 · ed25519-dalek 2.2.0 · **libcrux-ml-kem 0.0.9** (Cryspen, F\*/hax formally verified;
ml-kem 0.3.2 kept as a differential-KAT oracle) · chacha20poly1305 0.10.1 · hkdf 0.12/0.13 · hmac
0.12/0.13 · sha2 0.10/0.11 · blake3 · subtle 2.6.1 · zeroize 1.8.2 · arti-client 0.42 · rustls 0.23.40.
Supply chain is gated in CI (cargo-audit, cargo-deny, osv-scanner, Trivy, version floors, daily cron).

## 7. How to build & run (reproducible)

- Toolchain: Rust 1.96, edition 2024. `cargo test --workspace` runs the full suite.
- Desktop release (Windows, cross-compiled from Linux): the committed, secret-free
  `scripts/build-windows-release.sh` (zeroed PE timestamp, remapped paths, `SOURCE_DATE_EPOCH`; bundled
  C hardened with `-fstack-protector-strong -D_FORTIFY_SOURCE=2`). See [`../DEPLOYMENT.md`](../DEPLOYMENT.md).
- **Reproducibility (verified byte-deterministic).** Repeated builds of the same source + toolchain
  produce a **byte-identical** binary — confirmed by rebuilding twice and comparing SHA-256. The
  **published binary is config-free**, so there is now **one reproduction path for everyone**: run the
  committed recipe (`scripts/build-windows-release.sh`) at the matching commit + toolchain and the
  resulting `pvtcoms.exe` matches the single published SHA-256 — **no secrets needed**, because no
  per-deployment relay config is baked into the binary. This proves the published artifact is exactly
  this source with no hidden backdoor.
  - **Connecting:** the exe reads its relay config at runtime, in priority order, from (1)
    `PVTCOMS_RELAY` / `PVTCOMS_RELAY_KEY` env vars, then (2) a `pvtcoms.conf` **sidecar** next to the exe
    (`relay=<onion>`, `key=<hex>`, optional `pow=<n>`). Invited members receive `pvtcoms.conf` alongside
    the exe; it carries the shared access bearer secret and is never part of the published binary, the
    repo, or git. (This supersedes the earlier baked-config / two-mode scheme — SR-2026-06-05-008.)
- Tor transport: `--features tor` (needs a real network; see [`../../STACK.md`](../../STACK.md)).

## 8. Existing test coverage (what the auditor inherits)

- **204 core unit tests + 13 proptest properties** + integration suites: RFC KATs (ChaCha20-Poly1305
  RFC8439, HKDF RFC5869), a **differential FIPS-203 KAT** (libcrux vs RustCrypto ML-KEM byte-identical),
  **Wycheproof** AEAD/curve vectors (`core/tests/wycheproof.rs`), an **at-rest no-leak** suite
  (`at_rest_no_leak.rs`), an **output-hygiene** suite (`output_hygiene.rs` — scans `client/src` for
  secret-interpolating log sinks), and wire-parser **fuzz** tests (no-panic on thousands of hostile
  inputs). Every core module is **0-surviving under cargo-mutants** (whole-core sweep).
- **37 client tests + 5 FFI tests.** clippy/fmt/cargo-audit/cargo-deny clean. **269 tests pass total.**
- **Local end-to-end testbed:** `scripts/audit/local-testbed.sh` spins up a relay + two profiles (alice,
  bob) and runs the full async-onboarding + offline store-and-forward flow on localhost (no Tor needed;
  set `PVTCOMS_RELAY_POW=8` for speed; the script pins `PVTCOMS_DATA_DIR` to stay hermetic), then shows
  how to open each side's GUI. A deterministic repro of the offline/relay/SQLCipher path in ~10s.
- **SBOM:** full dependency tree in [`dependency-tree.txt`](./dependency-tree.txt) (`cargo tree`); exact
  pins in `Cargo.lock`.
- **Gaps (please probe):** no cargo-fuzz/Miri in CI yet (loom IS used for the concurrency registry); Tor
  integration tested manually (chutney/arti integration not in CI); no formal protocol model. The
  opt-in cover-deposit timing mask and the relay health endpoint are new (v0.1.38–0.1.39) and
  lightly exercised — see §10/§11.

## 9. Suggested engagement

- **Type:** combined **cryptographic design review** + **code audit** + light **protocol/metadata**
  threat-modeling. (No external infrastructure to pentest — there is no server.)
- **Rough size:** ~12k LOC Rust; the crypto + offline + relay subsystems are the dense parts.
- **Deliverables:** findings with severity (CVSS or firm scale), reproducer where applicable, and a
  **retest** after remediation. We will fix and request re-verification (we have a habit of it — see the
  prior-findings log in [`SECURITY_CLAIMS_AND_LIMITATIONS.md`](./SECURITY_CLAIMS_AND_LIMITATIONS.md)).
- **Disclosure:** coordinated; see [`../../SECURITY.md`](../../SECURITY.md) for the reporting channel.
  AGPL-3.0 — the source is fully available.

### Engagement logistics _(maintainer fills in before sending to vendors)_

| Field | Value |
|---|---|
| Frozen commit / tag | annotated tag `audit-0.1.40` (created; code identical to `e85f05f`) |
| Desired start / timeline | _[maintainer to specify — e.g. "Q3, ~2–3 week window"]_ |
| Budget range | _[maintainer to specify]_ |
| Deliverable format | Findings report (severity-rated, with reproducers) + **retest** after remediation |
| Communication | A lead engineer is available for real-time Q&A (shared channel) throughout |
| Access | Public repo + this package + `_internal/DESIGN.md`; the testbed needs only a Rust toolchain |
| Re-audit cadence | Re-review after each material crypto/protocol change (we ship small, reviewed slices) |

## 10. The things we most want checked

1. The **hybrid KEM binding**: does `hybrid_session_key` bind the full transcript (both public keys **and**
   the ML-KEM ciphertext) so the session is MAL-BIND-secure? (We fixed a binding bug here — re-verify.)
2. **AEAD nonce management** everywhere: confirm no nonce is ever reused under any key (chat ratchet,
   offline send-chain, at-rest sealing, envelope). See the prior nonce-reuse fix.
3. **Metadata at the relay**: can a malicious/observing relay link sender↔recipient, or two messages to
   the same conversation, from tokens / sizes / timing? Now includes the **cover-deposit** scheme
   (v0.1.39): does a self-deposit to `cover::self_deposit_token` (device-secret-derived) plus its later
   reclaim look like an ordinary conversation token to the relay, and does the loop actually defeat
   send-activity fingerprinting to the degree §11/claims state (and no more)?
4. **The hand-written wire parsers** (`wire.rs`, relay/offline framing): memory/DoS safety on hostile
   bytes; tag-verified-before-use.
5. **At-rest + keystore + data-dir data-safety**: SQLCipher keying/PRAGMAs; the DPAPI/Keychain/
   Secret-Service migration logic; **and the new (v0.1.36) app-data-dir migration** — can any
   interrupted/partial move of `paths.rs::migrate_legacy_cwd_data` lose or duplicate an identity, or
   leave a half-moved profile that silently starts fresh? (It is designed delete-only-after-durable +
   fail-closed; confirm.)
6. **Transport fail-closed** (v0.1.35): is there *any* path where a non-onion relay address is dialed
   directly (IP exposure) in a Tor build, outside the loopback / explicit-opt-in carve-outs? Is any
   transport metadata written to disk by default (the diagnostic log is now opt-in)?
7. **Relay operability surface** (v0.1.38): the `/health` endpoint is loopback-only **enforced in code** —
   confirm it cannot bind/serve off-loopback and leaks only aggregate (never per-token) counters. The
   `relay2=` failover must never silently redirect an explicitly-chosen relay.

## 11. Delta log — what changed since this package was first authored (v0.1.8 → v0.1.39)

The cryptographic and protocol **core is unchanged** from the audited design (handshake, ratchet,
offline send-chain, prekeys, relay tokens, directory records, at-rest sealing). The changes below are
in the transport, storage layout, operability, and the wiring of two already-designed privacy
primitives. Each shipped with tests; an auditor should weigh the new attack surfaces.

| Version | Change | New/changed code | Why it matters to the audit |
|---|---|---|---|
| 0.1.35 | **Transport fail-closed hardening.** The Tor diagnostic log (`pvtcoms-tor.log`, arti circuit/guard/HS internals) is now **opt-in** (`PVTCOMS_TOR_DEBUG_LOG=1`) — nothing written to disk by default. A **non-onion** relay address in a Tor build is now dialed over a Tor exit circuit, never direct TCP, except loopback or an explicit `PVTCOMS_ALLOW_DIRECT_TCP=1`. | `client/src/tor.rs`, `client/src/mailbox.rs` (`open_relay`, `env_flag_on`) | Removes transport metadata from the at-rest surface; closes an IP-exposure path. Directly relevant to claim **M2** and **Z1**. |
| 0.1.36 | **Per-profile state moved CWD → per-user app data dir** with a one-time, fail-closed migration (durable-copy-then-delete; never clobber; idempotent; exits rather than starting a fresh identity beside a stranded one). | `client/src/paths.rs` (new) | A new data-safety/migration surface — same class as the keystore/SQLCipher migrations in §10.5. |
| 0.1.37 | **`demo/` crate renamed to `client/`** (`pvtcoms-demo` → `pvtcoms-client`). The arti HS nickname is intentionally **kept** as `"pvtcoms-demo"` (it keys the on-disk HS keystore; renaming would change every user's `.onion`). | whole `client/` crate; `HS_NICKNAME` const | Pure rename + path churn; no behavior change. The "demo-crate maturity" caveat in the claims doc is retired — this IS the shipped client. ADR-011 records why obfs4 stays an external lyrebird binary. |
| 0.1.38 | **Relay operability:** loopback-only `/health` endpoint (aggregate counters via `core::relay::Relay::stats()`), client `relay2=` failover, encrypted onion-key backup/restore (`deploy/backup-onion-key.sh`). | `core/src/relay.rs` (`stats`), `client/src/mailbox.rs` (`failover_candidate`, health server), `deploy/` | New local/operator surface — confirm the health endpoint is loopback-enforced and aggregate-only; confirm failover never redirects an explicit relay choice. |
| 0.1.39 | **Two designed-but-unwired privacy primitives wired in:** (a) **cover deposits** (opt-in `PVTCOMS_COVER_TRAFFIC=1`) — constant-cadence cover blobs to the user's own `cover::self_deposit_token`, reclaimed on poll; (b) **directory auto-publish** on going online. | `core/src/cover.rs` (`self_deposit_token`), `client/src/offline_client.rs` (cover loop), `client/src/dir_client.rs` (`auto_publish_in_background`) | New metadata-resistance machinery. Honest scope (see claims §3): cover masks idle-vs-active + deposit volume; it does **not** equalise an individual send's timing. Confirm the self-token is unguessable by the relay/circle and that the scope claim is neither over- nor under-stated. |

**One-line summary for a returning auditor:** the crypto you were going to review is the same; what's
new is (1) transport now provably fail-closed with no default on-disk metadata, (2) a new data-dir
migration to data-safety-check, (3) a loopback-only relay health/failover/ops layer, and (4) an opt-in
cover-traffic timing mask with a deliberately bounded claim.
