June 19, 2026

No oracle: why every locked message looks the same

Most of the ways encryption leaks are not breaks of the cipher. They are oracles — small, honest-looking signals that tell an attacker whether a guess was warm. A padding oracle that distinguishes “bad padding” from “bad MAC”. An error that says wrong password instead of wrong username. A key hint in a header that whispers this block is for Alice. None of these hands over the plaintext directly; each one narrows the search until the plaintext falls out on its own.

YGOOW’s message format — we call it Variant C — is built so there is nothing to ask. Here is the whole thing:

blob = nonce(12) ‖ AES-256-GCM(K, nonce, plaintext)
K    = SHA3-256(key_material)

That is the format. No key id. No version-of-key marker. No “this belongs to conversation #7”. No hint about the length or kind of key. The block carries the 12-byte nonce, the AES-GCM output, and its 16-byte authentication tag — and not one bit that points at the right key.

You don’t look up the key. You try.

There is no “find the key that decrypts this”. The client takes the keys it already holds — a password you both typed, a file you both have, a link, a random key from an invite QR — and trial-decrypts. AES-GCM’s tag is the gate: the right key yields a valid tag and the plaintext appears; any other key fails the tag and yields nothing.

try_decrypt(blob, my_keys):
    for k in my_keys:
        pt = AES-256-GCM(SHA3-256(k)).decrypt(blob)   # InvalidTag -> None
        if pt is not None:
            return pt
    return None        # -> shown as [locked]

The important part is the failure. A wrong key returns None. A key you don’t have returns None. A message that was simply never meant for you returns None. All three are the same event, and the GCM tag makes them so: forging a valid tag without the key has a probability of about 1 in 2¹²⁸. There is no partial plaintext to skim on the way to failing — GCM authenticates before it reveals, so a wrong key gives you a closed door, never a half-open one.

Why that matters past the math

Where it ends — because we always say so

Trial-decryption is honest about its costs. It is O(keys you hold) per block: a bound set by the size of your own vault, not a weakness, but real. It protects the content of a message, not the fact that one exists — a block’s size and timing are still visible to whoever holds the store (we wrote about exactly that in the whitepaper’s traffic-analysis section). And it leans entirely on AES-GCM being used correctly — above all, never reusing a nonce under one key. That last point is why long-running conversations advance a forward-secrecy ratchet instead of trusting a single static key forever.

Verify it on your own phone

You do not have to take our word for any of this. The app ships an on-device self-test that runs this precise property as a known-answer check: encrypt with one key, try to decrypt with the wrong one, and confirm the result is [locked]no error, no oracle. Tap the flask in the app and watch it pass, against the same test vectors the reference implementation uses.

Your key, your rules — everything else is redacted.


← Back to blog