Whitepaper

v0.5 · 2026-09-17

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

2. Identity & key exchange

Your identity is a set of keypairs — Ed25519, X25519 and a post-quantum ML-KEM-768 key — all derived locally from one seed 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:

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.

On the phone, AES-256-GCM runs in a native library (ring) on the processor’s own AES and carry-less-multiply instructions, called through FFI from every place the app encrypts — including background workers. It replaced a pure-Dart implementation after we measured that one leaking timing (§11): the output is byte-identical, and the app’s self-test shows which implementation is actually running.

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, or burn-after-read) — 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 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.

Group rooms use per-sender keys with a forward-secret chain per sender, signed epoch changes for every membership change, and the same size padding and message conditions as one-to-one conversations. They are a deliberate trade: a room gives up the deniability of a one-to-one channel for who said what among its members, and the app says so before you create one. The honest edge today: a membership change reaches the members who open the room while it is fresh (roughly half an hour); members who don’t, stay on the old epoch until the room is re-keyed with both sides present. Delivering those changes in the background is being redesigned together with its metadata cost, which we measured (§7) — so the app’s removal dialog states this limit instead of promising more.

The prekey-server problem applies here too, and gets a different answer than it does for PCS above. Your identity carries its own long-term ML-KEM keypair, derived from the same seed as your Ed25519 and X25519 keys and travelling in the same contact bundle — so the first message encapsulates to a key that has nothing to do with the X25519 secret, and still nothing is deposited on any server. This matters precisely because of the threat being defended against: if the post-quantum starting key were derived from the X25519 shared secret, then breaking X25519 would hand over both halves of the hybrid at once for the first chain, which is the exact window a harvest-now-decrypt-later adversary is recording. An earlier revision of this design did derive it that way; the reasoning that justified it, and the measurement that overturned it, are written up in the changelog entry below.

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 addresses themselves never expire or appear together. What rotation does and does not buy against the relay is a measured question, and the measurement is in §7 — the short version is that it limits what an outside observer gains from one address, and does not stop the relay from grouping a device’s addresses by their timing.

7. Traffic analysis — the metadata that remains, measured

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. Since v0.3 we have also stopped arguing about this layer and started measuring it: each mechanism below was tested against the strongest adversary we could build from our own public protocol description, with the acceptance criteria written down before the run. Where the numbers overturned a claim this document used to make, the claim is corrected here and listed in the changelog.

The relay can still observe size, timing, and volume — over Tor the source is hidden, the event is not. We narrow each, and we say how far:

So the corrected statement of what the relay learns is this. It cannot read a word, does not see an IP, and holds no name, key, or contact list. It can — from timing alone, today — keep a persistent pseudonym for a device across address rotations and see, per epoch, which pseudonymous devices exchange frames and roughly how many conversations each keeps open — and, for a room, how many members it has and when its membership changes. Address rotation still bounds what an outside slip reveals: a malicious contact or a one-off timing correlation on the network ties you to one address for one epoch, not to a fixed handle. Two designs that close the timing channel at the relay are measured and under decision — independent schedules per address, or a single fixed-rate channel per device with addresses multiplexed inside it (which closes all four measured axes with one change). Neither is claimed here until it ships and is measured again.

What remains beyond that 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. At the guard relay our own shaping changes nothing measurable — what helps there is Tor’s own netflow padding — and cover traffic raises the bar without removing it.

The honest summary: YGOOW makes who you are and what you said unavailable, and it makes who you talked to a pseudonymous timing inference rather than a record. It does not claim to beat an adversary who already watches the entire network — no usable messenger does — and it no longer claims to beat its own relay’s stopwatch until the mechanism that does so is measured.

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 profile while the real one stays hidden in the same store (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.

A hidden volume is easy to make deniable on the day it is created and hard to keep deniable while it is used, so we measured it over a lifetime: scripted months of ordinary use on the production storage code, against someone who copies the phone’s storage once, or twice days apart, and someone who times the unlock. The first measurement (v0.4 era) failed the design: after 30 days a single copy revealed the hidden profile in 69 % of simulated phones whose decoy was never opened, two copies in 98–100 %, and unlocking the real profile took about 1.1 s longer than anything else. The redesign was measured the same way:

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. The backup file

A profile can be exported to a file so a lost phone does not mean a lost identity. The file is one sealed block: Argon2id with 256 MiB of memory and 4 passes derives its key from your secret, on top of the memory-hard step already applied to the secret itself — so one guess costs an attacker about 3.6 GiB of memory traffic, which caps a top-end GPU at a few hundred guesses per second. We chose those parameters by measuring, on the reference phone, that memory is cheap for the owner (allocation is ~3 % of the time) and expensive for the attacker (whose cost grows with the square of memory), after an earlier revision of ours had lowered memory and raised passes on a mistaken argument — see the changelog. Restoring a profile takes about six seconds on that phone.

The file is padded to a multiple of 64 KiB, and whether it was sealed with a password or with a file is byte-for-byte indistinguishable. Measured: its size reveals nothing about the number of contacts (up to ~324), conversations, rooms, or the structure of your unlock conditions. It does reveal the order of magnitude of your message history — 1.5 of 2 bits, and no padding scheme within a sane budget hides it — which is why the export offers identity and contacts only, without history, a file that is both smaller and silent. Because a copy can be attacked offline forever, sealing it with a typed password passes a stricter gate than the rest of the app: an estimator that treats a dictionary word as a word rather than as letters. Measured against password classes of known construction, “word + digits + symbol” passwords now get past it without a weak-password warning in 0.3–0.6 % of cases (97 % with the old meter), and 0.21 % of a leaked-password corpus does. Below the threshold the app warns and asks you to confirm; above it, it still reminds you that the number is a ceiling — it never silently accepts a typed password, and never blocks one. Its known blind spots are named in the dialog: keyboard walks and letter-for-digit swaps can still pass, and random 8-character secrets draw a warning in about 71 % of cases — a warning you can override, not a block. The limits we cannot design away: the file is not bound to a device, and neither the YubiKey factor nor the decoy profile applies to it — a copy is opened by the secret alone, with as many attempts as the attacker’s hardware allows.

10. What YGOOW does not protect

We state these plainly — because the projects that don’t are the ones you shouldn’t trust.

11. 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.

Measured, not assumed

Between August and September 2026 we ran a series of pre-registered measurements against our own implementation on the target hardware — an ARM64 Android phone, release build — rather than on a developer’s desktop. The method is the same each time: criteria and predictions written down before the run, a null control and a positive control in every comparison, three replications, and the ledger of which predictions missed published with the result. The ledger is not flattering (one card records four misses to one hit) and that is the point of keeping it.

None of this is an audit, and we do not call it one. It is the work an audit would check, done in the open before anyone is asked to check it.

12. Dead Drop — a note for someone without the app

A Dead Drop is a one-time text note shared by a link or QR code: no account, no contact exchange, and the recipient does not need the app — the link also opens in a browser at /d. The design follows from three threats.

In a browser two things are weaker than in the app, and the page says so: its code is served by us each time it loads, and over a normal connection our server sees the visitor’s IP (Tor Browser and our onion address remove the second). The browser client uses Web Crypto for AES-GCM and randomness, plain JavaScript for SHA3 and HKDF — so notes without a password still work in Tor Browser at “Safer”, where WebAssembly is off — and a pinned Argon2id build only when a password is set. Both clients are pinned to the same test vectors, and the browser one re-runs them for anyone at the self-test page. “Gone after reading” is the relay’s promise, not a proof — which is why the note is encrypted anyway.

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”)

Key material

Identity & key exchange

Forward-secrecy ratchet

Double Ratchet (plain-channel contact mode)

Post-quantum hybrid (X25519 + ML-KEM-768)

Quorum decryption (Shamir)

Signatures (group rooms)

Wire framing

Backup file

Dead Drop

Every value above is exercised by known-answer test vectors on both implementations. The app’s on-device self-test re-runs the core of them — key derivation and the message cipher, the handshake and safety number, the forward-secrecy ratchet and the no-oracle property — so you can confirm the math on your own phone, not just take our word for it. (The ML-KEM and Double Ratchet vectors run in the test suites, not yet on the device; we say so rather than round it up.)

Changelog

A design document that quietly rewrites itself is worth less than one that says what it got wrong.

v0.5 — 2026-09-17 — the decoy measured over a lifetime, a leak fixed, and one new disclosure.

  1. “Indistinguishable on disk” was true on the day you set it up. §8 used to say the decoy left no on-disk evidence of the hidden profile. Measured over months of simulated use, a single copy of storage revealed it in 69 % of phones after 30 days (87–88 % after 90) when the decoy was never opened, and two copies in 98–100 %. The same measurement found that heavy use of the decoy could overwrite the hidden profile — real data loss. Redesigned and measured again: no data loss in 600 simulated lifetimes, equal sizes, and detection from one or two copies without the decoy password down to a single residue that is now closed by construction. What remains with the decoy password is stated in §8 rather than implied away.
  2. Unlock time gave the hidden profile away. Opening the real profile took ~1.1 s longer than opening the decoy or a phone without one — fully separable in three runs. It is now within tens of milliseconds on the reference phone.
  3. The AES-GCM timing leak published in v0.4 is fixed with a native implementation, verified on the phone against the old library as a positive control in the same session (§11). The sentence “we make no constant-time claim for message encryption” is replaced by a claim scoped to the hardware path it was measured on.
  4. The backup gate no longer waves “Password1!” through — a stricter estimator for typed passwords, which flags that class as weak in all but 0.3–0.6 % of cases (§9). The in-app meter stays a ceiling.
  5. Long messages are split, not refused — numbered parts, each padded to the top size class (appendix).
  6. New disclosure: group rooms leak their size and membership changes to the relay when opened or changed (§7), and a membership change reaches only members who open the room while it is fresh (§5). Neither was claimed otherwise here; both are now written down.
  7. Dead Drop (§12): one-time notes by link, in the app and in a browser.

v0.4 — 2026-09-10 — five corrections from measurement.

  1. The relay can group a device’s addresses. §6 used to say that per-conversation phase-shifted rotation meant the relay “cannot batch addresses into one device”, and §7 that a correlation “confirms at most one 15-minute window”. We measured our own background schedule against a relay computing likelihood ratios over the public protocol: 85–90 % correct grouping after one epoch, 99.8 % within 30 minutes, resume timings 96–98 % after three resumes, and a thousand devices do not dilute it. An earlier fix (randomised, staggered polling) had looked sufficient under a weaker adversary — 1.9–3.1× chance — and that number turned out to be a property of the measure, not of the schedule. The claim is withdrawn; §7 now states what the relay learns and what is under decision to close it. The per-persona claim (“personas don’t reveal that they share a device”) falls with it, since the poll round covered every identity at once.
  2. Cover traffic hides which frames are yours, not that you are talking. “The relay cannot tell when you actually send” is replaced by the measured statement: 0.0018 bit between real and decoy frames inside an open conversation, grid re-phased at every rotation, and 98.5 % of timing information retained by the relay when cover is off — which it is by default.
  3. The location condition is gone. A message or app-lock could be sealed to open only near a place. Evaluating it asked Android for a “network” position, which on a phone with Google services sends the surrounding Wi-Fi and cell identifiers to Google through the system network stack — outside Tor, attributed, at the moment of reading. The feature was removed entirely (code, permissions, envelope v4); its opcode is permanently retired. Written up on the blog.
  4. The backup KDF was lowered on a wrong argument, then restored. In late August we moved the file’s Argon2id from 256 MiB / 4 passes to 128 MiB / 6 passes, reasoning that iterations would recover the cost. They do not: the attacker’s cost scales with memory squared times passes, so the change had cut it by 62 %. The premise that 256 MiB might not fit was also false by a factor of four (1 GiB ran without failure on the reference phone). Restored to 256 MiB / 4 on measurement; the false sentence was removed from our internal documents as well.
  5. The padding claim and the strength meter were overstated. Our documents said the backup file’s 64 KiB padding “hides the scale of the profile”; it hides contacts, rooms and condition structure, not history (1.5 of 2 bits leak), so an export without history was added. The entropy meter was described here as “honest”; measured, it overstates structured passwords by a median 45 bits. It is now a ceiling and labelled as one (§11).

v0.3 — 2026-08-23 — the post-quantum starting key no longer comes from the X25519 secret.

Until this revision, the responder’s starting ML-KEM keypair was derived deterministically from the X25519 ECDH secret two contacts already share. The argument for it was that this secret never crosses the wire, so a passive recorder cannot reconstruct it — and that argument was wrong, not in its premise but in its conclusion. That secret is a function of both parties’ long-term X25519 public keys, and two supported flows put those keys on channels an adversary can record: adding a contact remotely by passing their code through some other app, and an invite that carries the sender’s identity. An adversary who records that today and breaks X25519 later recovers the shared secret, and with it the post-quantum starting key — so for the first chain the hybrid did not hold “if either primitive holds”; both fell together. That first chain is also not necessarily short: a ratchet step happens on receipt, so a conversation nobody replies to stays in it entirely.

The property did hold for contacts added face to face, where those public keys never reach a recordable channel. It does not now depend on that distinction: each identity carries its own long-term ML-KEM key, derived from the identity seed and independent of X25519.

Two consequences worth stating plainly. The safety number now covers the whole bundle rather than only the Ed25519 key — previously, comparing digits with a contact would not have detected a substituted key-exchange key, which is the thing a remote-add attacker would substitute. And a contact whose bundle predates this revision falls back to the old bootstrap; the app says so on that channel instead of quietly claiming a protection it is not providing.

Living document, v0.5 — the full protocol specification will follow as YGOOW matures.