Posts  / #POST-225575
REDDIT

PhantomChat v3.0.0: PQXDH (ML-KEM-1024 + X25519) + MLS RFC 9420 + Monero stealth addresses in Rust — pre-audit, looking for crypto review

R
Apr 25, 2026 · 20:29

Built and shipped a desktop messenger that stacks several privacy primitives in one wire format:

\*\*Envelope (Monero stealth model)\*\*

\- Recipient publishes (view\_pub, spend\_pub) as \`phantom:<view>:<spend>\`

\- Sender's ephemeral r:

\- tag = HMAC(HKDF(r × view\_pub, "ViewTag"), epk)

\- enc\_key = HKDF(r × spend\_pub, "Envelope")

\- ciphertext = XChaCha20-Poly1305(enc\_key, payload, aad=tag)

\- Receiver scans every envelope on the relay with view\_secret (O(1) ECDH+HMAC per envelope), opens matching ones with spend\_secret

\- Relay structurally cannot link sender↔receiver because every envelope on the wire looks identical

\*\*PQXDH hybrid envelope (v2)\*\*

\- When recipient address carries ML-KEM-1024 pub (\`phantomx:\` prefix):

\- enc\_key = HKDF(spend\_shared || mlkem\_shared, "PhantomChat-v2-HybridEnvelope")

\- Both X25519 and ML-KEM must break to recover

\- Wire-version-byte distinguishes v1 (classic) from v2 (hybrid) — fully backward compatible

\*\*Ratchet bootstrap via ECDH commutativity\*\*

\- Sender: ratchet\_secret × recipient.spend\_pub

\- Receiver: recipient.spend\_secret × sender.ratchet\_pub

\- Same shared secret, no extra handshake needed

\- Receiver initializes both recv-chain AND send-chain up front so first reply encrypts immediately

\*\*Sealed Sender (Ed25519)\*\*

\- Identity sig over \`ratchet\_header || encrypted\_body\`, carried INSIDE the AEAD payload

\- Relay never learns sender — only recipient verifies after decrypt

\- Padded to 1024-byte blocks against length correlation

\*\*Groups: BOTH Sender Keys AND MLS (RFC 9420 via openmls 0.8)\*\*

\- Wire-version-byte dispatches: 1 → Sender Keys (small groups, fewer deps), 2 → MLS TreeKEM

\- MLS pinned to \`MLS\_128\_DHKEMX25519\_AES128GCM\_SHA256\_Ed25519\` so it reuses our existing X25519+Ed25519

\- File-backed openmls storage — groups survive app restart

\- Custom \`StorageProvider\` wraps \`MemoryStorage\` + atomic \`persist()\` snapshot via bincode

\*\*Contact discovery: DDH-PSI over Ristretto255\*\*

\- 3-round protocol, only the intersection leaks

\- Domain-separated hash-to-Ristretto so PSI points can't collide with other subprotocols

\- \~150 LOC on curve25519-dalek

\*\*Transport\*\*

\- libp2p GossipSub (P2P direct) + Nostr relays (WebSocket/TLS) + StealthRelay (SOCKS5→Tor→TLS→WS)

\- MultiRelay with SHA256 LRU dedupe (4096 envelopes)

\- Per-relay auto-reconnect with jittered exp-backoff (cap 60s)

\- Dandelion++ stem-then-fluff (p=0.1, 10-min stem rotation)

\- Cover traffic CSPRNG-filled (Daily 30-180s, Aggressive 5-15s)

\*\*Selftest\*\* runs all 9 cryptographic phases in one process — 30 checks, no network required. 64 integration tests green (\`cargo test --no-default-features --features net,mls\`).

\---

\*\*Pre-Audit warning\*\* is prominent in README + LICENSE. Krypto-stack is spec-implementiert + integration-test-covered, but NOT externally audited. Looking for serious crypto review specifically around the COMPOSITION of these primitives (individual primitives are NIST/RFC standards).

\*\*Specific review-questions where I'd love eyes:\*\*

1. \*\*Sealed-sender attribution timing\*\* — sig verification is constant-time (subtle crate)? Currently using \`ed25519\_dalek::Verifier::verify\` — anyone seen documented timing leaks there?

2. \*\*Stealth-tag scanner\*\* — the per-envelope ECDH+HKDF+HMAC is constant-time on the matching path, but does the early-return-on-tag-mismatch leak any information about view\_secret to a sufficiently patient timing attacker on the relay?

3. \*\*MLS-WLC2 wire prefix wrapping\*\* — I wrap MLS Welcome inside our sealed envelope before fan-out to known group members. Is the embedding (length-prefix + bytes verbatim) safe, or should I derive a separate AAD from the MLS group\_id?

4. \*\*PSI cross-protocol confusion\*\* — using \`domain\_sep + raw\_bytes → Ristretto255 hash-to-curve\`. With domain separation, the PSI points should never collide with stealth-tag points, but is there a more standard construction (OPRF-based)?

5. \*\*PQXDH hybrid alignment\*\* — combining via \`HKDF(spend\_shared || mlkem\_shared)\` follows the obvious composition. Any reason to prefer salt-based combination instead?

Repo (MIT): [https://github.com/cengo441337-a11y/phantomchat](https://github.com/cengo441337-a11y/phantomchat)

Spec: [https://github.com/cengo441337-a11y/phantomchat/blob/main/spec/SPEC.md](https://github.com/cengo441337-a11y/phantomchat/blob/main/spec/SPEC.md)

Architecture / threat model: [https://github.com/cengo441337-a11y/phantomchat/blob/main/docs/SECURITY.md](https://github.com/cengo441337-a11y/phantomchat/blob/main/docs/SECURITY.md)

Coordinated disclosure: [admin@dc-infosec.de](mailto:admin@dc-infosec.de)

Will respond to all comments within \~30 min during waking hours (Berlin TZ). Thanks for any time you spend on this.

Post image