how it works

How the deniability actually works.

No hand-waving. The exact mechanism that lets one encrypted file decrypt to two true answers, the distinguishers an honest tool has to close, and the one we leave to you.

the gap

The problem encryption doesn't solve.

Normal encryption keeps a file secret from anyone without the key. It does nothing once someone can make you hand the key over.

A seized device, a border stop, a subpoena, a compromised AI agent, a wrench. The moment you decrypt, the real plaintext is on the table. The encryption did its job perfectly and you're still exposed.

Deniable encryption attacks the other half of the problem. The goal is not "they can't read it." The goal is "when they make you open it, what they get is indistinguishable from a believable lie, and there is no way for them to prove a second answer exists."

the mechanism

One ciphertext, two true answers.

deny.sh splits a normal encrypted file into two pieces: the ciphertext and a separate control file. You need both to decrypt. The same ciphertext can be paired with different control files, and each pairing decrypts to a different, complete, valid plaintext.

The mechanism is deliberately simple, because simple is auditable:

1

Stretch the keys

Your two passwords are hashed and stretched with Argon2id into a 256-bit key.

2

XOR, then encrypt

Your plaintext (with a hidden length marker) is XORed with the control file, then encrypted with AES-256-CTR.

3

Reverse to read

To decrypt, you run it backwards: AES-decrypt, then XOR back against the control file to recover the plaintext.

Here is the part that matters. XOR is reversible in both directions. Given a fixed ciphertext and key, any plaintext you want can be turned into a matching control file by XORing it against the decrypted intermediate value.

So the "deny" operation generates a second control file that makes the very same ciphertext decrypt to a decoy of your choosing. There is no cryptographic record of which control file came first. Both are equally valid algebraic derivations from the same bytes.

That is the whole secret. Not a hidden volume, not a secret second key buried in a header. Just XOR, applied so the real answer and the decoy answer are mathematically symmetric.

honest distinguishers

"Provably deniable" is the easy part. Staying deniable is the hard part.

The cryptographic core gives a clean theorem: an adversary holding the ciphertext, both control files and both passwords still cannot tell, from the maths, which decryption is real. That much is genuinely provable, and we prove it in the whitepaper.

But an honest tool can't stop there, because a real adversary doesn't only look at the maths. They look at the files and they look at the contents. Every one of those is a potential tell. We went looking for every distinguisher we could find and closed the mechanical ones. Here they are, in the open.

1. File size of the control files

Closed

If your real control file was 512 bytes and your decoy was 97 bytes, an adversary doesn't need to break anything. The bigger one is real. We make every control file for a given ciphertext exactly the same length, byte for byte. There's a regression test that fails the build if they ever differ.

2. The length of the ciphertext

Closed to a coarse band

Even with the length marker hidden inside the encrypted zone, the size of the encrypted file leaks how long your real message was. That's enough to tell a 12-word seed phrase from a 24-word one. So for the tools that produce standalone files an adversary might measure, we pad the encrypted payload up to a coarse size band (64, 256, 1024, 4096, 16384 bytes). The padding is random, sits inside the encrypted zone, and is stripped losslessly on decrypt.

We are precise about what this buys you: the file size now reveals only which band you're in, not the exact length. A small, deliberate leak remains. We'd rather tell you that than pretend the channel is fully closed.

3. Whether the decoy passes its own validity check

Closed

This is the subtle one, and it's the one most "fake data" generators get wrong. A real credit card number passes the Luhn checksum. A real 24-word seed phrase has a valid BIP-39 checksum. A real IBAN satisfies mod-97. If your decoy only looked like a card number but failed Luhn, an adversary runs the checksum and instantly knows it's the fake.

So deny.sh's decoy engine doesn't just match the shape, it generates decoys that pass the same integrity check a real value would:

TypeReal-world check the decoy also passes
Credit cardLuhn (mod-10)
Seed phrase (12–24 words)BIP-39 checksum
IBANISO 7064 mod-97
Bitcoin WIF keyBase58Check
Solana key64-byte Base58 decode
Ethereum key32-byte hex
UK NHS numbermod-11

A decoy that fails its checksum is rejected and regenerated until it passes. So when someone coerces a decoy decryption out of you, the seed phrase they get is checksum-valid. They can load it into a wallet. It just isn't the one with money in it.

4. Whether the decoy is plausible in context

Your job

This is the one we cannot close for you, and we won't pretend otherwise. The maths can make a decoy checksum-valid and the right size. It cannot judge whether "a checksum-valid credit card number" makes sense as the thing you'd actually have stored there, or whether your decoy message reads like something a real person would hide. A decoy that says "this is the decoy" is perfectly encrypted and completely useless.

Deep validity is necessary, not sufficient. Choosing a believable decoy is the human half of deniability, and it stays with you.

when it matters

What this means when it actually matters.

Picture the file leaking, or the device getting seized, or the agent getting prompt-injected into dumping its secrets.

The adversary has the ciphertext and a control file. They run it. They get a complete, valid, checksum-passing, correctly-sized plaintext: your decoy. There is no larger "real" file sitting next to it, no length giveaway, no failed checksum, no second header hinting that more exists. From where they stand, the bytes they recovered are simply the answer.

You can't be compelled to reveal something an adversary can't prove exists. That's the whole point of building it this way, and it's why we close the mechanical tells in the open instead of leaning on a claim of perfect secrecy.

verify it yourself

Don't take our word for it.

The construction is public, the SDKs are open, and the maths is written up in full. Read it, run it, and check the claims on this page against the code.