Whitepaper
A short, honest overview of how YGOOW works and, just as importantly, where each protection ends. Nothing here asks you to trust us; it asks you to verify.
1. Design goals
- No identity on the server. No accounts, no phone numbers, no user table. The relay cannot know who talks to whom.
- Access by key, not by permission. Who can read a message is decided entirely by who holds the key — never by a role or a list on our side.
- Metadata minimisation. The network layer (Tor v3) hides your IP and location; the application layer stores only ciphertext and timestamps.
- Honesty about limits. Every layer is documented together with what it does not protect.
2. Identity & key exchange
Your identity is an Ed25519 / X25519 keypair derived locally and held only on your device. A password does not derive the identity — losing the device loses the identity by design. One install can hold several independent identities behind a single app-lock, each with its own keys and contacts and nothing linking them.
Optionally, a YubiKey (over NFC) can be required as a second factor for the app-lock: its HMAC-SHA1 challenge-response — computed on the key and never leaving it — contributes one secret to the Argon2id key alongside your password, so unlocking needs both. It is a local, offline access gate, not a per-message signature; the slot is set up once with ykman.
Conversation keys are exchanged offline by default — in person, by shared file, or by a QR scan — which removes an entire class of man-in-the-middle attacks. A remote, network-assisted exchange is available as a deliberate, clearly-labelled choice, with a fingerprint (safety number) to verify later. Once two people are contacts, a conversation needs no room code at all: both sides derive the same session identifier and channel key from their X25519 ECDH secret (bound to both identities), so writing to a contact is the invitation — and the relay sees only an unguessable id it cannot tie to either party.
3. Message cryptography — “Variant C”
Each message is sealed with AES-256-GCM under a key derived from your chosen secret:
- every secret is measured for its real strength; low-entropy inputs — a short password, a small or guessable file, a predictable link — are stretched with Argon2id (memory-hard) before use, while high-entropy material is hashed with SHA3-256, and the weakest factor is never allowed to hide behind the strongest;
- there is no key-wrapping and no key hint in the ciphertext. The client simply trial-decrypts with the keys it holds. A wrong key yields a locked block — no error, no oracle.
Because every message can use a different key, the same room looks different to each participant: some lines are text, others are just a lock.
4. Quorum decryption (Shamir)
A message can be split so it only opens when K of N people combine their keys (Shamir’s Secret Sharing over GF(2⁸)). One infiltrator holding one share learns nothing.
5. Conversation modes, forward secrecy, and post-compromise security
A message passes through up to five independent layers — transport (TLS), Tor, the end-to-end channel, optionally locked content, and optionally a set of conditions (a lifetime, an open-after date, burn-after-read, or a location) — and each layer above the channel is a deliberate choice. There is no insecure default: you select a conversation’s protection before it begins (see choosing your protection). Those conditions travel inside the sealed block, so the relay never sees them; they are checked on the recipient’s device — an honest client-side policy, not something the server can read or enforce against a determined holder.
Forward secrecy is always present. For an ongoing conversation a symmetric ratchet advances the key per message and erases the used key, so compromising the current state does not retro-actively expose earlier messages.
Post-compromise security (PCS) — a conversation healing itself after a state compromise — is provided where the channel itself is what protects you, and is not over-claimed where it is not. The plain-channel contact mode runs a Double Ratchet: every reply mixes a brand-new key-exchange result into the conversation root, so an attacker who steals your state loses the conversation again after one full round-trip. The locked-content modes do not depend on it — there the content is sealed under a separate secret the channel never holds, so a compromised channel does not read the words at all.
The honest edge: PCS needs entropy the attacker has never seen, and we have no prekey server to hand it to us (§6 — the relay is deliberately incapable of holding anything on your behalf). So the responder’s starting ratchet key is derived deterministically from the ECDH secret the two of you already share, which means the very first chain is only as fresh as that root. It heals after one full round-trip, once each side has contributed a key the other has never seen. This is a real trade against designs that keep one-time prekeys on a server — we take it deliberately, and we write it down rather than round it up.
Post-quantum. An adversary who cannot break X25519 today can still record your ciphertext and wait (“harvest now, decrypt later”). The contact channel therefore runs a hybrid: each ratchet step mixes an ML-KEM-768 (FIPS 203) shared secret alongside the X25519 one, combined so the result stays secure as long as either primitive holds — ML-KEM is added to X25519, never substituted for it. The same prekey-server problem applies and gets the same answer: the starting ML-KEM keypair is derived on both devices from the shared ECDH secret, never deposited anywhere, with the same first-chain edge as above. The post-quantum material rides in every message of an epoch rather than only the first — see §7 for why paying that bandwidth is the point.
6. Transport — onion + store-and-forward
The relay is reachable as a Tor v3 onion service, so the server’s location and your IP are hidden, and the connection is authenticated by the onion address itself. The asymmetry matters: the relay is the onion service; you are a Tor client reaching it, never a standing service of your own — which removes the surface that has deanonymised peer-to-peer onion messengers, where each user is a fixed service an attacker can locate by watching its guard nodes (see the Ricochet lesson). Tor is the default; the app can also connect over Tor bridges (for censored networks) or plain clearnet, but leaving Tor is a deliberate, labelled step the app makes you confirm — because off means the server sees your address.
Delivery is store-and-forward: messages wait, encrypted, until the recipient reconnects — across a dropping Tor circuit and without either party being online at the same moment. The server keeps no per-recipient state; the client carries its own cursor. The address of a conversation is not a fixed handle: by default it is re-derived on a short clock (a new, unguessable id roughly every 15 minutes), and each conversation’s rotation is phase-shifted by its own shared key, so the relay cannot batch them into “these addresses belong to one device.”
7. Traffic analysis — the metadata that remains
A deaf relay hides content and who-talks-to-whom — but nothing that actually delivers messages can hide that traffic exists at all. We’d rather name what’s left, and what we do about it, than imply it’s nothing.
The relay can still observe timing (when a block is uploaded or fetched — over Tor the source is hidden, the event is not), size, and volume. We narrow each:
- Size — every message is padded to a fixed size bucket by default, so the relay sees only a handful of sizes, never the exact length (size is the strongest per-conversation fingerprint). The same rule decided how the post-quantum material travels: it would be cheaper to send the ML-KEM key and ciphertext only when a ratchet step happens, but then that message would be a different size — and the relay could read the rhythm of your key changes straight off the wire. So the post-quantum header is carried in every message of an epoch, at a constant ~2.3 KB. We pay the bandwidth so that there is no size class to notice. That is the same trade as padding itself: a fixed cost, instead of a secret signal.
- Address stability — the conversation address rotates on a ~15-minute clock with a per-conversation phase offset, so a single timing correlation, or a malicious contact, confirms at most one 15-minute window — not the conversation’s history.
- Send timing — an optional high-risk mode sends on a fixed cadence and fills the gaps with indistinguishable decoy frames, so the relay cannot tell when you actually send. Each identity also rides its own Tor circuit, so personas don’t reveal that they share a device.
What remains is the hard limit of every low-latency anonymity network: a global passive adversary who can watch both ends of a Tor circuit at once can still attempt timing correlation. The cadence mode raises that bar; nothing eliminates it.
The honest summary: YGOOW makes who you are and what you said unavailable. Who you talked to is not provable from the server alone — and even server logs combined with one external slip-up expose at most a single short epoch, not the relationship. It does not claim to beat an adversary who already watches the entire network — no usable messenger does.
8. The device, and being forced to open it
The profile is encrypted at rest under your app-lock; the screen is blocked from screenshots and the OS “recent apps” preview. For a forced unlock, two tools shift the choice to you: a decoy — a second password opens a separate, empty profile while the real one stays hidden in the same store, with no on-disk evidence that a hidden profile exists (a VeraCrypt-style hidden volume) — and a panic lock that disarms biometrics so a compelled fingerprint opens nothing. Biometric unlock, when set up from the decoy, can only ever open the decoy.
These narrow what coercion reveals. But deniability is a credible denial with no on-disk proof against it — not invulnerability: a determined coercer can keep pressing on suspicion (the feature is public), and cryptography does not survive a wrench. They also do not survive live spyware, and cannot hide that the app is installed — which is exactly the limit we name next.
9. What YGOOW does not protect
- A compromised device. Spyware, or a phone taken while already unlocked, reads your screen regardless of any cryptography. No messenger survives this; the decoy and panic lock above help only before that point.
- The fact that you run the app. YGOOW hides content and metadata, not the existence of the app on your phone.
- Endpoint key hygiene. Weak shared secrets, or keys handed to the wrong person, defeat the math.
We state these plainly — because the projects that don’t are the ones you shouldn’t trust.
10. Verify, don’t trust
The cryptography is implemented against known-answer test vectors and a reference implementation. As the project matures we will publish the protocol specification and seek independent review.
Appendix — cryptographic parameters
The exact primitives, as implemented in the reference and ported verbatim to the client. Every domain-separation salt is constant by design: a shared key must reproduce the same bytes on every device, so a per-device salt is not an option — and the secrecy of these salts is not what provides security (it never is). Known-answer test vectors pin every value below.
Message AEAD (“Variant C”)
blob = nonce(12) ‖ AES-256-GCM(K, nonce, plaintext); 16-byte GCM tag; no key hint anywhere in the block.K = SHA3-256(key_material).- Nonce: 96-bit, random per message. The random-nonce birthday bound is ≈2³² messages per key — comfortable for the short-session profile, and the reason long-lived conversations use the ratchet rather than one static key.
Key material
- Low-entropy input (password, short text, a small or guessable file / link) → Argon2id, salt
ygoow/redact/argon2/v1, time = 3, memory = 64 MiB, parallelism = 1, 32-byte output. Memory-hardness is what blunts precompute against the constant salt. - High-entropy input (a large random file, a random QR key) → SHA3-256.
- The client estimates entropy to route between the two, and surfaces the result as an honest strength meter — the weakest factor never hides behind the strongest.
- Combined sources → HKDF-SHA3-256 over length-prefixed inputs (4-byte big-endian prefixes remove the
a‖bvsa‖bambiguity), saltygoow/redact/combine/v1.
Identity & key exchange
- 32-byte CSPRNG seed → HKDF-SHA3-256 → Ed25519 (signing / fingerprint) + X25519 (ECDH). The seed never leaves the device.
- Remote exchange: X25519 ECDH → HKDF-SHA3-256 bound to both public keys (sorted, so the result is identical for either side).
- Fingerprint / safety number: SHA3-256 over the sorted pair of Ed25519 keys → 8 groups of 5 digits.
Forward-secrecy ratchet
- Symmetric KDF chain (HKDF-SHA3-256, salt
ygoow/redact/ratchet/v1), a separate chain per direction. mk_n = HKDF(ck_n, "mk"),ck_{n+1} = HKDF(ck_n, "ck"); used keys are erased (forward secrecy). The 4-byte message number is bound as AEAD associated data; out-of-order delivery is tolerated up to 1000 skipped keys; replays are rejected.- Post-compromise security is scoped, not skipped: the symmetric chain gives forward secrecy but not PCS by itself. The locked-content modes do not need it — their secrecy lives in a key the channel never holds — while the plain-channel contact mode runs the Double Ratchet below.
Double Ratchet (plain-channel contact mode)
header = dh_pub(32) ‖ PN(4) ‖ N(4)— 40 bytes, bound in full as AEAD associated data;blob = header ‖ nonce(12) ‖ AES-256-GCM(mk, nonce, plaintext, aad=header).- Root step:
(RK', CK) = HKDF-SHA3-256(salt=RK, ikm=DH_out, info="dratchet/rk", L=64), withRK₀= the static ECDH secret. Because the root starts at a secret only the two static private keys can compute, a new ratchet key is authenticated transitively through the root — so rotating keys need no signatures, and the channel’s deniability survives intact. - Responder bootstrap:
boot_seed = HKDF-SHA3-256(salt="…/dratchet/v1", ikm=sk, info="bootstrap/responder", L=32)— both devices compute it, nothing is deposited on the relay. Roles are fixed without negotiation (initiator = lexicographically smaller static public key). - Skipped-key cache is hard-bounded:
MAX_SKIP = 1000per chain,MAX_SKIP_KEYS = 2000globally, FIFO eviction — out-of-order delivery over Tor is tolerated without giving anyone a memory-exhaustion lever. - Decryption is transactional: a DH step is taken on a copy of the state and committed only after the AEAD tag verifies, so a forged header cannot desynchronise a live session.
Post-quantum hybrid (X25519 + ML-KEM-768)
- ML-KEM-768, FIPS 203: encapsulation key 1184 B, ciphertext 1088 B, shared secret 32 B. Category 3 — the same parameter the industry picked for hybrids (X-Wing, TLS
X25519MLKEM768). - Combiner:
HKDF-SHA3-256(salt=<context>, ikm = ss_x25519 ‖ ss_mlkem ‖ ct_mlkem, info="…/pqhybrid/v1"). A standard dual-PRF construction: secure if either input holds. The ML-KEM ciphertext is bound into the input as well, which closes re-encapsulation subtleties independently of the primitive’s binding properties. - In the ratchet, the current root key
RKis the HKDF salt — so the post-quantum step inherits the same signature-free, deniability-preserving authentication as the classical one (ML-KEM, like DH, produces no signature). - Hybrid header:
dh_pub(32) ‖ pq_ek(1184) ‖ pq_ct(1088) ‖ PN(4) ‖ N(4)= 2312 B, all of it bound as AEAD associated data, constant for every message in an epoch (§7). - Correctness is pinned against the published C2SP/CCTV accumulated vectors — 10 000 deterministic keygen/encapsulate/decapsulate operations reduced to one digest — recomputed independently by both our implementations, on top of our own cross-implementation vectors.
- Where it ends, plainly: our reference implementations (Python and Dart) exist to be checkable in a test VM on both sides — they are not constant-time, and the production target on the device is a vetted native implementation validated against these same vectors. Post-quantum protection here covers the conversation content; the rendezvous address is deliberately out of scope, as it is metadata rather than content.
Quorum decryption (Shamir)
- Secret sharing over GF(2⁸) (AES reduction polynomial
0x11B), byte-wise, K-of-N with 1 ≤ K ≤ N ≤ 255. Any K−1 shares reveal nothing. Shares are generated from the secret and distributed offline.
Every value above is exercised by known-answer test vectors, and the app’s on-device self-test re-runs them — so you can confirm the math on your own phone, not just take our word for it.
Living document, v0.2 — the full protocol specification will follow as YGOOW matures.