PRIVEX // security model

How the defenses actually work

A club that never checks ID
and keeps no cameras
but makes every stranger
prove they're not a robot.

Privex can't ban a troublemaker by name, email, or IP, because it deliberately never learns any of those. So every defense here works a different way: by cost, by shape, and by math — never by identity.

Toggle Plain / Technical above at any time — every section below has both versions.

01

The shape of the problem

Most apps stop abuse the easy way: they know your email, your phone number, your IP address — so they can just block you. Privex refuses to know any of that, on purpose. That's the whole point of the app.

Which means it can't build a blocklist. Instead, every gate in this system is built to ask a different question — not "who are you?" but "did you pay the right cost to get this far?" A real person pays it once, without noticing. A botnet has to pay it thousands of times, on purpose, in a way that gets more expensive the more of them show up.

Zero-knowledge architecture removes the usual anti-abuse toolkit (IP throttling, device fingerprints, account reputation) by design — the server has no identity signal to key on. Every public, unauthenticated endpoint is therefore either a) proof-of-work gated (cost-based), b) validated to an exact wire shape (structure-based), or c) capped by an aggregate, identity-free pressure counter in Redis. Authenticated endpoints get one more lever: a per-pseudonym rate limit keyed by an HMAC of the session token — never a raw user_id.

02

Adversaries this system was built for

Seven tiers of attacker, from a coffee-shop snoop to a nation-state with a court order. Each one gets a different answer when they try to break Privex.

TierAdversaryWhat they getOutcome
1ISP / DNS provider / Wi-Fi eavesdropperSees encrypted traffic only. Cannot confirm Privex is in use.BLOCKED — DoH + Nym mixnet route + no server-visible IP
2Active network MITMCannot read, modify, or replay any message undetected.BLOCKED — TLS 1.3 + Nym layers + HKDF auth tags on every message
3Compromised Privex server (hacked, insider, coerced operator)Full database access yields zero usable information about any user.BLOCKED — all content encrypted client-side; Sealed Sender hides sender; no PII in DB; UNLOGGED tables turn to ash on crash
4Nation-state with legal authority (subpoena, NSL)Even with a valid court order, there is legally nothing to provide.BLOCKED — Iceland jurisdiction; server holds no identities, no content, no social graph, no IPs; warrant canary signals coercion
5VPS host with physical access (RAM dump, disk read)Sees relay IPs, encrypted blobs, and pseudonymous IDs only.BLOCKED — all connections arrive via Nym/onion relays (no user IPs); DB holds only encrypted payloads; LUKS volumes at rest
6Future quantum computerCannot retroactively decrypt captured traffic.BLOCKED — CRYSTALS-Kyber-1024 + X25519 hybrid key exchange; Dilithium3 + Ed25519 hybrid signatures; AES-256-GCM symmetric
7Root-malware on endpoint deviceScreen content and in-memory messages are readable. Keys non-exportable.PARTIAL — WebCrypto non-extractable + WASM zero-fill after each op; SE/Keystore on mobile; kernel-level root is out of scope
03

How it all connects

One request, whether from you or a bot, flows through the same pipe. The pipe can't tell you apart by identity — so each stage checks something else instead.

YOU a real person, once ATTACKER / BOTNET indistinguishable, at first EDGE — REVERSE PROXY strict headers . CSP . access logs never written is this route public? PROOF-OF-WORK GATE public / target-revealing routes only SHA-256 pre-filter -> Argon2id (32 MiB, memory-hard) PER-PSEUDONYM LIMITER authenticated routes fixed-window, per token keyed by HMAC(session token) never a raw user id INPUT VALIDATION every field checked against an exact shape before use APPLICATION LOGIC SEALED SENDER unwrap — sender never stored KEY TRANSPARENCY inclusion-proof verify OPAQUE (opt-in) password never touches server SESSION CHECK fails CLOSED if Redis is down DATA LAYER Postgres (queue/blob/log tables vanish on crash) Redis (nothing written to disk, ever) -> nothing here is worth subpoenaing

The insight: your message content never even reaches this diagram unencrypted — it was already sealed shut on your device before it left. Everything above exists to stop abuse and stop leaks, not to protect what's inside the envelope. The envelope protects itself. The pipeline above governs abuse-resistance and metadata exposure, not confidentiality. Content is PQXDH+Double-Ratchet encrypted client-side (WASM) before a single byte reaches fetch() — nothing server-side ever holds plaintext, a key, or a sender identity to leak in the first place.

04

Ten lines of defense

Every layer in the diagram above, expanded — what it actually does and why it's built that way.

1THE DOOR PUZZLE

Proof-of-Work

Anyone can walk up and ask for a key or an account — Privex can't check ID, so instead it makes every knock cost real effort, like a bouncer who skips the ID check but makes you solve a puzzle first. One person barely notices. A thousand fake bots need a thousand solved puzzles, and the puzzles get harder the more bots show up.

Public, target-revealing endpoints (registration, key-bundle fetch, KT proof, OPAQUE login-init) require a solved hashcash-style challenge instead of an IP rate limit. As of the Argon2id hybrid layer: find a nonce whose SHA-256 hash clears a leading-zero-bit pre-filter, then run Argon2id (32 MiB, memory-hard) on that hash and clear a second bit target. SHA bits run 22->31, Argon2id bits run 1->4, both driven by the same aggregate Redis pressure counters — no per-IP or per-user state exists to key on.

2THE TAB

Per-user rate limiting

Once you're inside and chatting, Privex still watches how fast you're doing things — like a bartender who'll cut off someone ordering 200 drinks a minute. But your "tab" is just a random daily token, so the bartender can slow you down without ever learning your name.

Fixed-window limits keyed by HMAC(session token) — e.g. messages/send 120/60s, blob download 120/60s, spk/rotate 30/3600s, history delete 10/600s.

3NO CAMERAS INSTALLED

Zero-log infrastructure

Most services keep a guest list "just in case." Privex's building was built without the wiring for a camera system at all. No IP is ever written down, not even for a second. The important database tables are set up to simply vanish if the server ever crashes, rather than persist. A court order asking for footage gets the true answer: there isn't any.

message_queue, blob_index, kt_log, pow_challenges are UNLOGGED Postgres tables (no WAL, truncated on crash). Redis runs save "" / appendonly no. The reverse proxy never mounts an access-log sink. A private verification harness greps the source tree for ip_address and any user_id on a logging call.

4A PRIVATE LANGUAGE

End-to-end encryption

Even Privex can't read your notes, because you and your friend write in a language only the two of you agreed on, before the note ever left your hand. Part of that language was chosen specifically to resist a future computer powerful enough to break today's codes.

PQXDH (X3DH + CRYSTALS-Kyber-1024) key exchange — hybrid classical + post-quantum from day one. Double Ratchet gives forward secrecy and break-in recovery per message. Identity assertions are signed with both Ed25519 and Dilithium3; breaking the scheme requires breaking both primitives at once.

5THE BLIND DOORMAN

Sealed Sender

When a note is dropped off for you, the doorman only reads "deliver to" — never "from," because the sender's name is folded up inside the sealed note. The doorman genuinely cannot tell you who left it.

The sender's certificate is encrypted inside the payload with an ephemeral X25519 key, never carried on the wire. message_queue has no sender_id column at all — enforced by schema, verified by test.

6THE PUBLIC RECEIPT BOOK

Key Transparency

What stops someone from quietly swapping your friend's mailbox key for their own? Privex keeps a public, tamper-evident receipt book of every key ever issued. Your app checks it automatically — a swapped key either shows up in the book (and your app alerts you) or gets refused outright.

Append-only Merkle tree of every registration/rotation. Each key fetch returns an inclusion proof verified against a signed root — cached as a single O(1) snapshot per log generation, rebuilt only when the log actually grows. A substituted key must appear publicly or the client rejects it.

7YOUR OWN LOCKER

Client-side key storage & app lock

Your keys never leave your device, and while you're using the app they sit in a part of the browser's memory even Privex's own code can't read out — a locker only your fingerprint or passphrase opens. Lock the app and the locker snaps shut: the connection drops, the decoy traffic stops, and the token proving who you are is wiped from memory.

Master key: non-extractable WebCrypto AES-GCM wraps IndexedDB-stored session state. Passphrase path: Argon2id (32 MiB, t=3) key-wrapping. Biometric path (WebAuthn-PRF) is the brute-force-resistant option — the secret lives in hardware, OS-rate-limited. Locking tears down the whole live session: WS disconnected, cover traffic stopped, token cleared — re-triggers on bfcache restore and page-freeze so a backgrounded tab can't resurrect a decrypted view.

8A BLIND BAR TAB

OPAQUE password recovery

If you want to recover your account with a password, here's the trick: the server is built so it never actually sees your password, not even for a second, not even hashed. It's mathematically blind to it — so stealing the whole database still doesn't let anyone try passwords against it offline, unless they also steal a second secret kept elsewhere.

OPAQUE (Ristretto255 + Argon2id KSF) — the server stores only an OPRF record and an encrypted key envelope, never anything derivable from the password itself. Fully opt-in: disabling it hard-deletes the row rather than flipping a flag, so non-opted-in accounts are unattackable offline even after a full database theft.

9THE STREET GUARD

Browser security headers

Before a request even reaches the app, a guard at the street checks you're visiting the real building, not an impostor wearing its skin. Browsers are told: run nothing that wasn't part of the original build, never let this page be framed inside someone else's page, never leak which page sent you here.

Strict CSP (default-src 'none', script-src WASM-only, no inline JS), COOP/COEP (needed for SharedArrayBuffer), X-Frame-Options: DENY, Referrer-Policy: no-referrer, and a Permissions-Policy blocking camera/mic/geolocation until explicitly used. CORS and WebSocket origin allowlists are required non-empty at boot — an unset allowlist is a hard startup failure, never a silent fallback to allow-all.

10THE EXACT-SHAPE CHECK

Input validation

Every file ID, ticket number, or reference you hand over gets checked against an exact expected shape before it goes anywhere near a back room. A fake ID shaped like "redirect me to a private endpoint instead" is rejected on the spot, before it gets anywhere close to a door it could trick open.

Every wire field is validated to an exact shape before use — a chunk ID must be 64 lowercase hex chars (a SHA-256 shape) checked before it can reach a fetch URL, since a crafted value could otherwise drive an authenticated request to an arbitrary same-origin endpoint. UUIDs, history-blob IDs, share indices, and enum fields are all similarly pinned, client and server side.

05

Beyond messages: files, calls, groups, and trust

The ten layers above protect messaging. But Privex also sends files, makes audio/video calls, manages groups, and checks images for CSAM — all while keeping the same zero-knowledge promise.

AFILE ENCRYPTION

Server is a dumb locker

Files are chopped into pieces, each piece locked with its own key, then scattered across a storage room. The server has no idea what was uploaded, who sent it, or even that the pieces belong to the same file. The instructions to reassemble them travel inside an already-encrypted message, visible only to the recipient.

Content Encryption Key (CEK) generated client-side. File split into 4 MB chunks, each encrypted with AES-256-GCM using a unique HKDF-derived chunk key. Chunks stored at content-addressed IDs (SHA-256 of ciphertext). File manifest (with CEK) sent inside a Sealed Sender Double Ratchet message. Recipient unwraps CEK via X25519 DH with their identity key. SHA-256 integrity check on reassembly.

BCALL PRIVACY

Audio/video without the leak

Call setup happens through the same sealed message channel — so the server never knows a call even happened. The media itself is double-encrypted: once at the transport level (standard), then again at the application level with keys only the two of you hold.

Call signaling uses Sealed Sender messages (no call record on server). DTLS-SRTP for transport encryption, then SFrame (RFC 9605) for end-to-end frame encryption with keys derived from the PQXDH shared secret. TURN relay sees only DTLS-SRTP-encrypted streams, authenticated with ephemeral HMAC tokens (60s validity, no user identity).

CGROUP SECRECY

MLS at scale

Group chats use a tree-based system so adding or removing someone only costs a few operations instead of re-keying everyone. Eject a member and they instantly lose the ability to read new messages — no matter when they joined.

MLS protocol (RFC 9420) with a binary ratchet tree (TreeKEM). Operations are O(log N): adding a member updates log2(N) internal tree nodes. Each Add/Remove/Update creates a new epoch with a fresh group secret; previous epoch keys are permanently deleted. Up to 500 members with full MLS; 500-5000 uses Sender Keys model for practical scaling.

DCSAM PROTECTION

Client-side, zero-knowledge

Before an image leaves your device, it's checked against a known-blocklist using a protocol where neither the server learns about your image nor your app learns about the full blocklist. If the check passes, your app generates a tiny cryptographic proof that the check was done correctly — the server verifies the proof without ever seeing the image.

PDQ perceptual hash (256-bit, similarity-tolerant) computed client-side on every outgoing image. Private Set Intersection via OPRF on Ristretto255: client blinds the hash, server evaluates against NCMEC blocklist, client unblinds to check membership. Server learns nothing about the hash. If clear, client generates a Groth16 ZK proof (~256 bytes, <5ms verify) that the PSI check was performed honestly. Message rejected without valid proof.

ETRAFFIC COVER

Hide signal in the noise

Even if someone watches the wire, real messages are mixed with fake ones that look identical. The server handles decoy messages the same way as real ones — so a watcher can't tell which messages are genuine by watching the server's behavior.

Cover traffic sent on a Poisson schedule indistinguishable from real message timing. Decoy messages addressed to nonexistent mailboxes; server drops them indistinguishably from real delivery failures. All messages padded to fixed 1024-byte blocks. Jittered receipt acknowledgments (5s floor) prevent timing correlation of delivery confirmations.

06

The cost curve: why bots can't win

The proof-of-work puzzle doesn't just cost a fixed amount. It gets more expensive the harder the attacker pushes — exponentially — with no way to share the cost across parallel workers. The proof-of-work difficulty scales dynamically with aggregate request pressure. The compounding SHA-256 and Argon2id targets eliminate parallel amortization for botnets.

Drag to simulate attack size 10,000 bots/min
Normal user
1x
One puzzle, once. Solved in milliseconds. You never notice.
Single request. Solved in <50ms. Transparent to user.
Each bot pays
512x
The puzzle gets harder automatically as traffic spikes. They pay 512 times the normal cost. Difficulty: SHA 2³¹ + Argon2id 4-bit (memory-hard)
Total attacker cost
5,120,000x
Total penalty paid. They can't share the work — every single bot pays the full inflated price. Per-bot cost × fleet size. No amortization possible — every connection pays independently.
Why it compounds
Like a toll road that doubles its price every time the queue gets longer. One driver barely notices. A thousand drivers? Each one pays a full day's wages just to merge.

This is why a botnet can't win. The cost curve isn't a straight line — it's a cliff. Drag the slider above to see how the total attacker cost explodes as the fleet scales up. Every single bot pays the full inflated price independently. There's no bulk discount. No shared credit. No parallelism trick that helps. Aggregate Redis pressure counters drive a two-layer hybrid PoW: SHA-256 pre-filter (22–31 leading-zero bits) and Argon2id (32 MiB, 1–4 memory-hard bits). Both layers track the same 60-second sliding window. A sustained 10,000 bot/min attack saturates the ceiling: SHA at 2³¹ (512× harder than idle), Argon2id at 4 bits (16× harder). The layers compound, not add. An attacker cannot amortize — every connection pays independently.

07

Attack coverage map

Nine attack classes. Each one collides with multiple independent defenses. No single bypass breaks more than one shield — and every shield has a backup. Nine distinct threat vectors evaluated against the architecture. Defense-in-depth ensures no single point of failure; each attack class is mitigated by overlapping sub-systems.

full block — defense is complete
partial — mitigated, residual risk noted

Every attack hits a wall — never a single guard. To break Privex, an attacker would need to defeat every shield in a row simultaneously. Break one layer, and the others still hold. No two attacks share the exact same set of defenses, because each one works differently. This is a map of overlapping protections that cover each other's blind spots. Defense in depth validated against the codebase. All mechanisms are active in the current build. The model assumes every layer can fail independently, and relies on overlap rather than perfection. Each card above lists the specific mechanisms that block that attack class.

08

Attacker vs. defense

Fifteen ways someone might actually try to hit Privex, and the specific mechanism already standing in the way.

Botnet

Mass-register thousands of fake accounts

Register faster than a human ever could, using scripts across many machines.

Automated registration burst across many workers, no shared IP to throttle by.

+

Every account costs a solved puzzle that gets harder as the crowd grows.

Hybrid PoW: SHA pre-filter (22 to 31 bits) + Argon2id (32 MiB, 1 to 4 bits), difficulty auto-climbing from aggregate pressure — no per-IP state to evade.

Scraper

Enumerate who has an account

Probe ID after ID to build a map of who's using Privex.

Walk the key-directory fetch endpoint to test existence of arbitrary IDs.

+

Every single lookup costs a puzzle too — mapping the userbase gets expensive fast.

Key-bundle fetch is PoW-gated (same hybrid puzzle) plus a 30/60s per-target cap as defense in depth.

DoS

Force expensive checks by spamming junk

Send garbage answers on repeat, hoping the server does costly work anyway.

Submit invalid/near-miss PoW solutions to force repeated server-side hashing.

+

Every puzzle is single-use — even a wrong answer burns it, so it can't be replayed.

Invalid attempts still consume the single-use challenge; a concurrency semaphore caps how many memory-hard checks run at once, bounding peak memory.

Cracker

Brute-force a recovery password

Steal the database and try guessing passwords against it, offline, forever.

Offline dictionary attack against a stolen credential table.

+

There's nothing to guess against — the server never even saw the password.

OPAQUE: server stores an OPRF record, never a password-derived value; opt-in, hard-deleted on disable.

MITM

Swap someone's key mid-conversation

Quietly replace a friend's public key so messages get redirected.

Silent key substitution in the directory to intercept a session.

+

Every key is in a public receipt book. A swap shows up there, or gets refused.

Key Transparency Merkle log with client-verified inclusion proofs against a signed root.

Spoof

Forge a message from someone else

Send a message that appears to come from a user whose keys you don't hold.

Craft a Sealed Sender envelope with a fake sender certificate.

+

Every sender certificate carries two unforgeable signatures. A forged one is rejected on arrival.

Sender cert is Ed25519 + Dilithium3 dual-signed; recipient verifies both before accepting. KT log confirms the signing keys are authentic.

Subpoena

Seize or legally compel the server

Demand logs, message history, or user identities from the operator.

Legal or physical seizure of server/database contents.

+

There's nothing there. No IP, no names, no sender field — even the tables that would hold it are wired to erase themselves.

UNLOGGED queue/blob/log tables, no PII columns, Sealed Sender means sender identity is never server-side to begin with.

Theft

Steal an unlocked device

Walk off with a laptop or phone that's logged in.

Physical device compromise while the app session is active.

+

Locking the app doesn't just hide the screen — it tears the whole session down.

Non-extractable master key + Argon2id/WebAuthn-PRF wrap; lock disconnects the socket, stops cover traffic, clears the token from memory.

Watcher

Watch traffic to see who talks to whom

Sit on the network and count who's sending messages to whom, and when.

Passive traffic analysis of message timing, size, and pairing.

+

The server only ever sees a recipient, never a sender — and a constant stream of fake decoy traffic hides real activity in the noise.

Sealed Sender + Poisson-scheduled cover traffic + fixed 1024-byte padding. Caveat below — the connection endpoint itself is still visible in Phase 1.

Amplifier

Hammer a public endpoint to run up costs

Flood a free, public health-check to burn server and cloud-storage bills.

Unauthenticated flood of a health-check route that fans out to paid dependencies.

+

The answer is cached for two seconds — a thousand knocks in that window get one real check, not a thousand.

Readiness probe result cached process-wide (2s TTL), single-flighted so concurrent misses share one dependency check.

Harvester

Steal encrypted file chunks from blob store

Access the raw blob storage and download all encrypted chunks.

Compromise blob store credentials or exploit storage-layer vulnerability.

+

Every chunk is individually encrypted with its own key. Without the manifest (sent inside an E2EE message), chunks are indistinguishable from random noise.

Each 4 MB chunk encrypted with unique AES-256-GCM key derived from CEK via HKDF. Server has no file-to-chunk mapping. Chunks content-addressed by SHA-256 of ciphertext.

Replay

Resend a captured message to confuse ordering

Capture a valid encrypted message and re-inject it later.

Replay an old Double Ratchet or Sealed Sender message to the recipient's inbox.

+

Every message has a unique counter. The recipient's app will reject a duplicate or out-of-order replay immediately.

Double Ratchet message counter + prev_counter fields; Sealed Sender includes a 24h expiry on sender cert. Redis key for recent message IDs prevents replay within the delivery window.

Timing

Correlate message timing to identify conversation pairs

Watch both sides of a conversation and match send times to deduce who talks to whom.

Correlate send timestamps on Alice's outbound with delivery timestamps on Bob's inbound.

+

Delivery receipts are artificially delayed by random amounts. Cover traffic fills the gaps. The timing signal is deliberately degraded.

Receipt jittered with 5s minimum floor + Poisson distribution. Cover traffic on independent Poisson schedule. Fixed 1024-byte padding eliminates size correlation.

Rollback

Present an outdated KT log root to hide a key swap

Serve a stale signed tree root so the client doesn't see a newly-substituted key.

Freeze or roll back the Key Transparency log to a prior state where the victim's key was attacker-controlled.

+

The client verifies the log root against gossip consensus. A rollback produces an older root that doesn't match the current epoch.

KT root signed with log's epoch key; clients cache the latest known root and reject monotonicity violations. Gossip-based consistency check across independent sources.

CSAM

Distribute known CSAM through the platform

Upload and share known child sexual abuse material before detection.

Send images hashing to NCMEC blocklist entries, evading server-side scanning.

+

Every outgoing image is hashed locally and checked via a private set intersection. Matches are blocked before the image ever leaves the device.

PDQ perceptual hash + OPRF-based PSI on Ristretto255. Server learns nothing about non-matching images. Groth16 ZK proof attached to every image-bearing message; server rejects without valid proof.

09

What we're honest about

Privex's own documentation calls these out by name. Repeating them here on purpose — a security page that only lists wins isn't a trustworthy one.

!

Your ISP can still tell you're connecting to Privex — for now.

Phase 1 uses a direct WebSocket, not yet routed through the Nym mixnet.

They can't see what you're saying, who to, or when — but they can see the connection itself. Hiding that entirely is the next major milestone.

Content, sender, and contacts stay protected (TLS + Sealed Sender + no server logs); only the connection endpoint is visible on the wire. Full Nym gateway wiring is the headline Phase 2 deliverable.

!

The anonymity crowd is still small.

The onion relay network is a limited node set today.

Traffic-hiding tricks work better the more people are using them at once — like it's easier to blend into a packed room than an empty one. Privex is early, so the crowd is still growing.

3-hop circuits across a small operator set; anonymity-set size scales with adoption, not architecture alone.

!

A short PIN alone isn't unbreakable if your device is stolen.

Passphrase-only app lock has an offline-guessing ceiling.

A short passphrase can eventually be ground down by someone with your device in hand and time to spare. Turning on fingerprint/Face ID unlock closes this gap completely — that secret never leaves hardware.

Argon2id bounds but doesn't eliminate offline brute-force against a short secret. WebAuthn-PRF (hardware-backed, OS-rate-limited) is the only truly brute-force-resistant factor.

!

iPhone notifications are limited by Apple, not by Privex.

iOS background execution constrains closed-app push delivery.

Apple doesn't allow the kind of always-on background connection that would make closed-app notifications instant on iPhone, for any app — that's a platform rule, not a corner Privex cut.

No real closed-app Web Push was built deliberately — a subscription endpoint would require a third-party record that deanonymizes the pseudonym and leaks arrival timing.

!

Call privacy is weaker than message privacy.

TURN relay sees IPs of both participants during a call.

Your call content is encrypted (double-layered), but the TURN server that relays the media sees the IP addresses of both sides. This is the same tradeoff Signal, Wire, and every other E2EE calling app makes.

TURN authentication uses ephemeral HMAC tokens (60s validity, no user identity). TURN logging disabled. SFrame encrypts each frame end-to-end. Residual risk: the relay operator knows two IPs connected for a call.

!

Server-side history backup (if enabled) weakens forward secrecy.

Opt-in history storage is the only server-side plaintext-adjacent data.

If you turn on history backup, encrypted copies of your messages live on the server. Your password is still required to decrypt them, but the encrypted blobs exist to be attacked. Default is OFF.

History blobs encrypted with a key derived from OPAQUE master seed. Server holds encrypted blobs only. Default off with clear warning. High-threat users should leave it disabled.

10

Technical reference

The numbers behind the diagrams above, for anyone who wants to verify the claims rather than trust the metaphors.

Proof-of-work difficulty, by aggregate pressure

Recent activityFinal difficultySHA pre-filter bitsArgon2id bits
Normal (0-5 registrations / min)22211
Elevated (16-40 / min)25232
Heavy (101-300 / min)29263
Ceiling (301+ / min)31274

Per-pseudonym rate limits (selected)

RouteLimitWindow
messages / send12060s
messages / ack20060s
blobs — download12060s
blobs — upload / delete6060s
spk / rotate303600s
history — delete all10600s
OPAQUE — login init (per target)1060s
KT — proof fetch (per target)3060s

Cryptographic primitives

ComponentAlgorithmStandard
Key exchange (classical)X25519 + X3DHRFC 7748 / Signal PQXDH spec
Key exchange (post-quantum)CRYSTALS-Kyber-1024NIST FIPS 203
Identity signatures (classical)Ed25519RFC 8032
Identity signatures (post-quantum)CRYSTALS-Dilithium3NIST FIPS 204
Message encryptionAES-256-GCMNIST SP 800-38D
Ratchet / key derivationHKDF-SHA256RFC 5869
Sealed Sender encryptionXChaCha20-Poly1305libsodium
Proof-of-Work (Layer 1)SHA-256 hashcashAdam Back / RFC 8011-style
Proof-of-Work (Layer 2)Argon2id (32 MiB, t=3)RFC 9106 / PHC winner
Password recovery (opt-in)OPAQUE (Ristretto255)IETF OPAQUE draft
Group messagingMLS (TreeKEM)RFC 9420
AV call encryptionDTLS-SRTP + SFrameRFC 9605
File chunk encryptionAES-256-GCM per chunkHKDF-derived subkeys
CSAM perceptual hashPDQFacebook/Meta, Apache 2.0
CSAM ZK proof systemGroth16 (Circom 2.0)256-byte proofs, <5ms verify
App lock (PIN path)Argon2id (32 MiB, t=3)wraps WebCrypto AES-GCM key
App lock (biometric)WebAuthn-PRFWebAuthn Level 2

Service-level agreements for security properties

PropertyGuaranteeWhat it means for youMechanism
Content confidentialityAbsoluteNo one but the intended recipient can read your messages.PQXDH + Double Ratchet + Sealed Sender; server never holds a plaintext byte or a key.
Sender anonymityAbsoluteThe server never knows who sent a message.Sealed Sender: sender cert encrypted inside payload, no sender_id column.
Forward secrecyAbsoluteIf today's keys are stolen, yesterday's messages stay secret.Double Ratchet: message key deleted immediately after use; DH ratchet on every message.
Post-quantum securityAbsoluteHarvest-now-decrypt-later attacks fail by design.Kyber-1024 + Dilithium3 from session one; hybrid classical/PQ key exchange.
Key integrityAbsoluteKeys can't be swapped without detection.Key Transparency: append-only Merkle log with client-verified inclusion proofs.
Traffic analysis resistanceStrongWho you talk to and when is hidden in decoy traffic.Sealed Sender + cover traffic (Poisson) + 1024B padding + jittered receipts.
ISP-level usage detectionPhase 2Currently visible; Nym routing will hide it in Phase 2.Phase 1: direct WebSocket. Phase 2: Nym mixnet for network-layer metadata resistance.

Hardening changelog (referenced in source, by ID)

  • PVX-06 Revocation check fails closed if Redis is unreachable — never silently skips.
  • PVX-08 Auth-verify does equal work on an absent user — no existence timing oracle.
  • PVX-09 Empty CORS/WS origin allowlist is a hard boot failure, never allow-all.
  • PVX-10 File-manifest IDs validated to exact hex shape before ever reaching a fetch URL.
  • PVX-11 A persistently-failing cleanup sweep raises an alert instead of failing silently.
  • PVX-18 Blob/share IDs rejected unless they match a UUID or key shape.
  • PVX-20 Auth-verify capped at 5/60s per target — anti brute-force.
  • PVX-21 Domain-separated signing context stops cross-protocol signature reuse.
  • PVX-22 App-lock passphrase minimum raised after a 6-char PIN proved too weak offline.
  • PVX-23 Key-Transparency tree cached as an O(1) snapshot, not rebuilt per fetch.
  • PVX-24 Every Redis/HMAC key is a purpose-bound subkey — never the root secret.
  • PVX-26 Malformed inputs are rejected outright instead of throwing at render time.