Some checks are pending
ci / test (pull_request) Waiting to run
Wire format lives in docs/PROTOCOL.md so the handshake, invite, and frames can be read without the Rust. Threat model points at it. The in-house audit notes are a finding-status table, not a third-party audit. SDK README no longer claims the legacy concat invite signature is what we encode.
58 lines
4.6 KiB
Markdown
58 lines
4.6 KiB
Markdown
# OnionWire threat model (v1)
|
||
|
||
OnionWire is a two-party chat over Tor onion services. There is no chat server.
|
||
Wire format: [PROTOCOL.md](PROTOCOL.md). In-house review notes:
|
||
[SECURITY_AUDIT.md](SECURITY_AUDIT.md) — **not** a public third-party audit.
|
||
|
||
## Tor relays are not a chat server
|
||
|
||
Each install hosts its own v3 onion and dials friends’ onions through in-process Arti. Relays move cells; they never see plaintext, never store messages, and never hold a roster. Compromising a relay is not compromising OnionWire’s message store. The store is the sqlite file on the endpoint.
|
||
|
||
## Location updates are not a missing-person finder
|
||
|
||
A signed `loc` frame (`onion`, `ts`, `sig`) rewrites a friend’s locator **only if** that friend is already in the roster, the signature verifies against the pinned pubkey, and `ts` is newer. There is no directory, DHT, or introduction server. If you rotated while they were offline, they cannot find you until they F3-paste your new invite. OnionWire will not hunt for a missing person.
|
||
|
||
## Rotating the onion does not revoke a friend
|
||
|
||
`F4` changes the locator only. The identity key is unchanged. Anyone who already has your pubkey can still prove they are talking to you, and a later invite paste with the same `k` updates their row. To become a new person, `/wipe-all` (new identity key). There is no in-band unfriend/revoke in v1.
|
||
|
||
## Message bodies are encrypted at rest; keys are not
|
||
|
||
Chat bodies in sqlite are ChaCha20-Poly1305 (`nonce || ciphertext` in the `messages.plaintext` column) with AAD `owmsg1 || friend_id_le64 || dir || 0x00 || row_id_le64`. Swapping ciphertext between rows fails closed. A random 32-byte data key is wrapped with Argon2id (same params as identity backup) from a non-empty passphrase. Salt + wrapped key live in `store_meta`. Unlock is fail-closed: wrong or empty passphrase does not open chat. Empty-AAD v0.2 blobs are rewrapped once on unlock; `list_messages` never falls back to empty AAD.
|
||
|
||
Identity secret key, friend public keys, and locators remain plaintext in the same db. The message key is not wrapped with `identity_sk` (that key is already on disk). A seized laptop still yields who you talk to and your identity unless you add OS/FDE. sqlcipher is out of v1. `/wipe` deletes chat and payments history (overwrite message bodies, `VACUUM`, WAL checkpoint); roster and identity stay. It is not a forensic erase — SSD wear-leveling can keep copies. `/wipe-all` deletes the data dir (new identity); same disk caveat.
|
||
|
||
## Fail closed
|
||
|
||
Peer onion down → send fails. Fingerprint mismatch vs the pinned key → hard fail, no send. Arti HS experimental: if it cannot publish, OnionWire stops; it does not fall back to C-tor.
|
||
|
||
## Profile is friend-visible, not a directory
|
||
|
||
A signed `prf` frame is shown to people who already have a session with you. Apply only for existing friends. There is still no name lookup, DHT, or public profile server. Anyone who already has a Noise session can see the profile you send them; that is not confidentiality against that friend.
|
||
|
||
## Monero sidecar is not a wallet
|
||
|
||
OnionWire never holds spend keys. Optional `ONIONWIRE_WALLET_RPC` talks HTTP Digest to a user-hosted `monero-wallet-rpc` on loopback (`--rpc-login` required; open RPC is refused). A Noise friend can sign any `rcp`; the signature proves who sent the claim, not that a payment happened. `verified=1` only after a conjunctive RPC match: one `get_transfers` row with the same non-empty `txid`, `amount`, and `address`. Incoming `rcp` stays `verified=0` if RPC is down, errors, or no exact row. Subaddress reuse is the user’s wallet policy.
|
||
|
||
## Backup file is the identity
|
||
|
||
`/backup` writes the identity and Noise static secrets. Treat the file like `onionwire.db`. Restore overwrites self keys and does **not** rewrite the friends table.
|
||
|
||
## Rate limit is availability, not anonymity
|
||
|
||
Incoming rendezvous accepts are token-bucket limited (30/60s, burst 10). Excess is dropped without handshake. That is a DoS/availability control. It does not hide that you run an onion, and it is not a traffic-analysis defense.
|
||
|
||
## Network (UDP / clearnet)
|
||
|
||
The application does not open UDP sockets and does not resolve friends via
|
||
system DNS. Peer dials are Arti `connect((onion, 80))` only. The one
|
||
intentional clearnet socket is optional `monero-wallet-rpc` on **loopback TCP**
|
||
(numeric loopback preferred; `localhost` still does `getaddrinfo`).
|
||
|
||
Do not add STUN/ICE/WebRTC, a DHT, SOCKS/C-tor fallback, or “direct TCP when
|
||
the onion is slow.” Those are the Tox-over-Tor leak class.
|
||
|
||
## Out of v1
|
||
|
||
Prosody, XMPP, s2s, MAM, carbons, outbox, multi-device, DHT / name server,
|
||
sqlcipher, A/V.
|