---
title: pvtcoms — Security Claims, Limitations & Prior Findings (for audit)
status: pre-audit
last_verified: 2026-06-12
verified_version: 0.1.48
---

# Security Claims, Limitations & Prior Findings

For the auditor: the properties we **claim** (each phrased so it can be validated or refuted), what we
**don't** claim, self-identified weaknesses, and a log of issues we already found and fixed (so you can
confirm the fixes and look for siblings).

## 1. Claims to validate or refute

Each row is a testable assertion + where it's enforced. **C** = confidentiality, **I** = integrity,
**A** = authenticity, **M** = metadata/anonymity, **S** = at-rest, **L** = local.

| # | Claim | Where |
|---|---|---|
| C1 | Message plaintext is end-to-end encrypted; no operator/relay/network party can read it. | handshake + ratchet |
| C2 | **Post-quantum**: a harvest-now-decrypt-later adversary with a future quantum computer cannot recover session keys (hybrid X25519 + ML-KEM-768; never classical-only, never PQ-only). | `pqkem`, `handshake` |
| I1 | Any modification of ciphertext/wire bytes is detected before use (AEAD tag verified first). | `crypto`, `wire` |
| A1 | After first contact, the peer's long-term identity is pinned; a swapped key is detected (loud "changed"). | `contacts`, `handshake` |
| A2 | The handshake is MAL-BIND: the session key binds both public keys **and** the ML-KEM ciphertext. | `handshake::auth_transcript`, `hybrid_session_key` |
| F1 | **Forward secrecy**: compromising current keys doesn't reveal past messages (ratchet; offline send-chain). | `ratchet`, `sendchain` |
| P1 | **Post-compromise security**: after a compromise, security self-heals once a fresh one-time prekey is mixed in; **fail-closed** if none (no silent downgrade). | `prekey`, `offline_client` |
| M1 | **Over Tor** (clearnet to the relay is never used — see M2), the relay learns no linkable sender↔recipient relationship from tokens/sizes/timing (rotating MAC tokens, padded buckets, R-derived envelope AEAD). | `offline`, `relay`, `mailbox` |
| M2 | Transport is anonymous (Tor v3 onion); the app's IP is never exposed to peers; **fail-closed** (no Tor→clearnet fallback). A non-onion relay address is dialed over a Tor exit circuit, never direct, except loopback / explicit `PVTCOMS_ALLOW_DIRECT_TCP=1` (v0.1.35). | `client/tor.rs`, `client/mailbox.rs::open_relay` |
| M3 | **No transport metadata at rest by default**: arti circuit/guard/HS diagnostics are written to `pvtcoms-tor.log` only when `PVTCOMS_TOR_DEBUG_LOG=1` (v0.1.35); otherwise nothing touches disk. | `client/tor.rs` |
| M4 | **Cover deposits** (opt-in `PVTCOMS_COVER_TRAFFIC=1`, v0.1.39): when enabled, the client emits constant-cadence deposits to its **own** device-secret-derived token, so an uplink observer can't tell idle from active. **Bounded claim** — see §3: this does *not* equalise an individual real send's timing. | `core::cover`, `client/offline_client.rs` |
| N1 | **No AEAD nonce is ever reused under any key** — chat ratchet, offline send-chain, at-rest sealing, envelope. | crypto-wide |
| S1 | At-rest data (history, identity, contacts, media) is encrypted; the device key is in the OS keystore (Windows DPAPI / macOS Keychain / Linux Secret Service), not a plaintext file — **except** the documented headless-Linux fallback to a 0600 dev-file key (see §3). | `histdb` (SQLCipher), `oskeystore` |
| L1 | The localhost UI can't be driven by a malicious website (same-origin/CSRF guard) and renders message content as plain text only (no HTML/script injection). | `gui.rs`, `gui.html` |
| Z1 | Secret key material is zeroized and not logged; no plaintext/keys/IPs/onion addresses/tokens appear in logs, panics, or crash dumps. | crate-wide (coding standards) |

## 2. What we explicitly do NOT claim (confirm we don't overstate)

- Defense against a **fully compromised endpoint** (malware/root on the device, keylogger, screen
  capture) — see THREAT_MODEL §4.
- Defense against a **global passive adversary** doing end-to-end Tor traffic correlation.
- Protection against **coercion / rubber-hose** (duress wipe is a v2 idea, not present).
- That first contact is MITM-proof **without** the user comparing the safety number out-of-band (TOFU).
- That the **bundled C** (OpenSSL/SQLite) is bug-free — we pin + monitor versions, not re-audit them.

## 3. Self-identified weaknesses / residual risk

- **TOFU at first contact** — the SAS is the only MITM defense; its effectiveness is UX-dependent.
- **No formal protocol model/proof** — correctness rests on tests + this review.
- **Classical ongoing ratchet** — PQ protects key *agreement*; the ongoing DH ratchet is X25519 in v1
  (PQ ongoing ratchet = documented v2).
- **Headless-Linux keystore degrade** — with no Secret Service, the device key falls back to a dev file
  (documented; OS-protected on desktop with a keyring/DPAPI/Keychain).
- **Client-crate maturity** — the shipped desktop app is the `client/` crate (renamed from `demo/` in
  v0.1.37); some CLI-helper paths are simpler than the GUI path. The audited `core/` holds the
  crypto/protocol; `client/` is the integration + transport + storage + UI layer.
- **Relay/offline subsystem is the newest** code and the densest metadata surface — highest-value review
  target. The relay operability layer (loopback `/health`, `relay2=` failover, v0.1.38) and the
  cover-deposit + directory-auto-publish wiring (v0.1.39) are the most recent additions.
- **Sender-activity timing — partially masked, opt-in.** Mailbox *polling* is jittered (`core::cover`, a
  randomized cadence so a fixed interval isn't a fingerprint), protecting the *receiver's* "when do I
  check for mail" metadata. **Cover *deposit* traffic now exists** (v0.1.39, opt-in
  `PVTCOMS_COVER_TRAFFIC=1`): a constant-cadence loop deposits indistinguishable blobs to the user's own
  reclaimed token, so idle and active periods look alike and the relay can't simply count real deposits.
  **Honest residual:** this masks idle-vs-active and inflates deposit volume; it does **not** yet
  equalise the timing of an *individual* real send (a send still rides its own immediate deposit). Full
  send-timing equalisation requires routing real sends through the slot cadence — a distinct
  "strict-send" lever (anonymity vs latency) that is **not** wired. Off by default (each cover deposit
  costs a real PoW). Do not assume an individual send is unobservable.
- **Relay is fail-closed under storage pressure** — the relay caps per-blob size, per-token entries,
  and total stored blobs (so a flooder paying PoW can't exhaust its memory). It **rejects** new deposits
  at the global cap rather than evicting old ones (evicting would mean silently dropping a legitimate
  user's mail). Trade-off: a depositor who can pass the gate (PoW + the shared access key on a gated
  relay) and is willing to burn PoW could fill the relay and deny new deposits to others until TTL
  expiry. Mitigated by gated access (invited circle only), the PoW cost, and the per-token cap; all
  tunable via `PVTCOMS_RELAY_MAX_*`. An operator needing strict availability over retention can lower
  the caps / shorten the TTL.
- **No external audit yet** — this package exists to fix that.

## 4. Prior findings we already fixed (verify the fixes + hunt siblings)

**Historical context to guide your threat modeling** — these are areas of past complexity and prior
blind-spots, each caught pre-release by adversarial self-review and fixed. You will audit the *current*
codebase against the spec (not validate our git history); this log simply points you at where the system
has been subtle, so you can probe for siblings of these classes.

| Issue | Severity | Fix |
|---|---|---|
| **MAL-BIND**: the hybrid combiner bound only the X25519 public keys, not the ML-KEM `ek`/ciphertext. | High | `hybrid_session_key` now absorbs the full transcript `x_R‖x_I‖ek‖ct`; +regression tests. |
| **At-rest AEAD nonce reuse**: mutable sealed blobs (contacts/store/identity) used a fixed nonce under a stable key → keystream reuse + forgery. | High | `seal_at_rest`/`open_at_rest`: per-write random salt → HKDF subkey, domain-separated; the zero nonce is then first-use only. See [`../reports/2026-06-02-aead-nonce-audit.md`](../reports/2026-06-02-aead-nonce-audit.md). |
| **Offline linkability**: clear `bundle_id` + length leak + burn-before-auth in the relay framing. | Med | R-derived envelope AEAD, id-free prekey nonce, trial-then-commit. |
| **SAS grindability**: 32-bit safety number. | Med | Widened to 64-bit (16 emoji); unified on the pairwise root. |
| **Wrong-message delete** (offline batch shares a ms timestamp). | Low | Delete matches `(ts, direction, text)`; unit-tested. |
| **At-rest data-loss** (keystore/SQLCipher migrations): non-atomic writes; deleting the legacy key before the new one is durable; re-import duplication; regenerating over an unreadable keyed store. | High (data-loss) | Atomic writes (tmp+fsync+rename); delete-only-after-durable-store + marker; idempotent migration via a flag committed in the same transaction; **fail-closed** on an unreadable keystore/DB (never regenerate over data). |
| **Localhost CSRF**: a malicious site could drive the local API. | Med | Same-origin/`Origin`/`Referer` loopback guard on state-changing requests. |
| **Transport metadata at rest** (v0.1.35): the Tor diagnostic log (`pvtcoms-tor.log` — arti circuit/guard/HS internals) was written next to the exe on **every** run, putting transport metadata on the at-rest surface the threat model protects. | High (metadata) | Gated behind `PVTCOMS_TOR_DEBUG_LOG=1`; nothing written by default. New error-pattern **EP-201** blocks re-introducing ungated "TEMPORARY DEBUG" diagnostics at commit time. |
| **Non-onion relay direct-dial** (v0.1.35): in a Tor build, a non-onion `relay=` address could fall through to a direct TCP dial, exposing the client's IP, despite the fail-closed rule. | High (deanonymisation) | `open_relay` routes non-onion relays over a Tor exit circuit; direct TCP only for loopback or an explicit `PVTCOMS_ALLOW_DIRECT_TCP=1`; strict `env_flag_on` parsing (fail closed). |

## 5. The questions we most want answered

1. Is the **hybrid handshake** sound and MAL-BIND-secure, with correct domain separation and no
   downgrade/reflection path?
2. Is **N1 (no nonce reuse)** true across *every* AEAD use, including the fixed-zero-nonce at-rest path?
3. What can a **malicious or passive relay** actually learn or link (the metadata claims M1)?
4. Are the **hand-written parsers** memory/DoS-safe on hostile bytes, with auth-before-use everywhere?
5. Can any **keystore/SQLCipher migration sequence** lose data, expose the device key, or fail open?
6. Does the **PCS fail-closed** logic actually prevent silent downgrade when no prekey is available?
