We run deny.sh on deny.sh
A credible security vendor uses its own product, on its own data, in production. Anything less is theatre. This write-up documents the internal control we ran against ourselves: we took a real operator credential (the GitHub recovery codes for our organisation account, the file that restores 2FA access if a hardware key is lost), migrated it into the deniable primitive we ship to customers, and ran a three-drill attack pass against it. The numbers and terminal output below are the actual artefacts of that pass.
This uses the same cryptographic primitive that powers the hosted runtime and the same SDK functions exposed in the public Apache-2.0 library. The wrong-password observability and at-rest properties are the properties customers receive from that primitive.
The setup
Operator recovery codes are the single highest-leverage credential in any organisation's infrastructure stack. They sit one layer above MFA: if the hardware keys break and the codes leak, the entire identity boundary fails open. The conventional storage answer is some combination of password manager, hardware vault, or a small encrypted file in an operator-controlled location, all of which assume the attacker never holds both the ciphertext and the right password at the same time.
The deniable primitive removes that assumption. Two passwords protect one ciphertext, which is paired with a small control file. The same passwords plus the real control file decrypt to the real plaintext. The same passwords plus a different control file decrypt to a plaintext you chose. An attacker holding the ciphertext and any one control file cannot tell, from the bytes alone, whether they are looking at the real artefact or a decoy.
For this exercise we used our internal credential-management tooling, which wraps the public SDK directly: the same encrypt, decrypt, and generateDeniableControl functions that ship with the Apache-2.0 release. We encrypted the recovery codes under two operator-held passwords, then generated a sibling decoy block: eight lines of plausible-but-synthetic codes formatted the same way GitHub formats theirs (xxxxx-xxxxx hex pairs). Same passwords, second control file, decoy plaintext.
The on-disk layout for one item:
~/.deny/items/GITHUB_RECOVERY_CODES.ct 338 B
~/.deny/items/GITHUB_RECOVERY_CODES.real.ctrl 4096 B
~/.deny/items/GITHUB_RECOVERY_CODES.decoy.ctrl 4096 B
~/.deny/items/GITHUB_RECOVERY_CODES.meta.json 412 B
Three observations. The ciphertext payload is compact, just the encrypted recovery codes plus a 48-byte header. The two control files are byte-identical lengths. And file(1) classifies both as featureless data: no header, no magic bytes, no metadata that distinguishes one from the other.
Drill 1. The wrong-password observability test
This is the test that matters most for deniability. If a normal password-protected file leaks, an attacker who tries to brute force it knows when they've succeeded: a wrong password gives an error or rejects the unlock, a right one opens the file. That signal is what makes brute force tractable. Try 10,000 passwords, the right one announces itself.
With deniable encryption, that signal should not exist. A wrong password pair produces output silently, with no error and no success tell. Where the attacker supplies a valid decoy control file, that output is a plausible decoy; with arbitrary wrong passwords it is indistinguishable random-looking bytes. Either way there is nothing to brute force against.
We ran the decryption pipeline five times. Once with the real password pair, four times with deliberately incorrect passwords drawn from a mix of pattern and random sources. Same binary, same ciphertext, same control file. The output of each run, truncated to 48 bytes for readability:
Attempt 1 returned the real plaintext, # GitHub 2FA Recovery Codes (deny-sh-crypto org). Attempts 2 through 5 each returned independent 48-byte sequences of unrelated content. No error code, no "invalid password" string, no retry counter, no rate-limit message. The decryption layer cannot signal failure because, mathematically, it has no notion of failure: every key produces an output, every output is a valid byte sequence.
This is the property that breaks conventional brute-force. An attacker without prior knowledge of the expected plaintext structure has nothing to test their guesses against. Even with prior structural knowledge, the deniable layer means any output matching that structure could itself be the decoy, the real artefact, or a second decoy the operator generated and never disclosed. The "right-answer" signal that anchors a brute-force attack is absent by construction.
Drill 2. The cost of guessing
The deniability property defends against an attacker holding the ciphertext without the passwords. The complementary question is the cost imposed on a structural attacker, one who has prior knowledge of the expected plaintext format and will pattern-match every decrypt against that format until something plausible emerges.
The key derivation function is Argon2id with parameters t=3, m=64 MiB, p=1, v=0x13. This is the documented interactive default across deny.sh; per-tenant configurations on the hosted runtime can be raised. We measured derivation cost on a single core of our production infrastructure:
2.26 derivations per second per core. Argon2id's memory-hardness penalises the parallelism that breaks faster KDFs because each concurrent guess needs a 64 MiB working set. Applied to two passwords, the floor of an attacker's task looks like this:
Two passwords double the cost. A motivated attacker who guesses both correctly still has the wrong-password observability problem from drill 1: they don't know they've succeeded. They have to take their candidate plaintext and decide whether it's "the real one" or "a decoy designed to look real."
Threat-model note
The numbers above are the floor. Higher Argon2id parameters, especially larger memory settings, raise the attacker's cost by orders of magnitude. The configuration we measure here is the interactive default, optimised for commodity client hardware. Enterprise tenants who accept higher decrypt latency in exchange for higher attacker cost can request elevated parameters at provisioning. None of these tradeoffs affect the deniability property in drill 1; they only affect the difficulty of the password layer.
Drill 3. The at-rest indistinguishability test
The last drill: prove that an attacker who seizes the encrypted files at rest, with no passwords yet, learns nothing about which file is real.
Identical lengths. Both classified as featureless data. No magic bytes, no headers, no version markers, nothing that file(1), binwalk, or any structural analyser can use to pick the real one. The bytes are statistically indistinguishable from random.
This is the property that makes the seizure case operationally meaningful. An attacker who exfiltrates one ciphertext and one available control file cannot determine from the bytes whether that control file is real or decoy. The filenames used in this writeup (.real.ctrl, .decoy.ctrl) are documentation labels only; in a production deployment the two control files sit across separated locations and trust boundaries, with the decoy positioned as the obvious one to find and the real one held behind an independent control. The indistinguishability of the control bytes is what this drill demonstrates; the separation of where they sit is a deployment property handled by the runtime.
What changed about our internal posture
The exercise drove a permanent change to our operational baseline. The plain-on-disk GitHub recovery codes are gone, shredded, and the real codes now exist only as a 338-byte ciphertext under the deniable primitive. Cold recovery was verified end-to-end (real passwords, real control file, real plaintext) before the original file was destroyed, with no fallback path left in place.
The same migration is in progress for the rest of our operator-class credential set: npm publish recovery codes, the GitHub personal access tokens used by CI, the Stripe live-mode root recovery, the AWS root account recovery codes, and the wallet seed phrases backing the company's on-chain treasury. Each is moving under the same primitive with its own dedicated decoy, generated to match the expected format of the real artefact closely enough that an attacker cannot triage real-vs-decoy on shape alone.
The compound effect is operational: every credential that, until last month, sat in some combination of password manager, plain-on-disk, or single-password encrypted vault is now stored under a primitive where seizure plus password exposure still does not yield the real artefact unless the attacker also identifies the correct control file. We could not credibly position deny.sh as enterprise infrastructure while leaving our own credentials on the wrong side of that line. They are now on the same side as our customers'.
Verifiability
Every number, terminal output, and behavioural claim in this post is reproducible against the public SDK. The cryptographic engine is a single source file (core.ts in the deny-sh-crypto/deny-sh repository), licensed Apache-2.0, with the algorithm written out in prose at deny.sh/verify and a public verification suite that executes in-browser.
To reproduce the drills against an installation under your control: install the deny.sh CLI, encrypt a small file with two passwords, generate a decoy with deny-sh decoy, and re-run the wrong-password loop and the Argon2id timing on your own hardware. The drill-1 and drill-3 outputs are deterministic up to entropy: the shape will reproduce exactly. Drill-2 numbers depend on the host CPU; expect a small constant multiplier in either direction.
For prospective enterprise customers running their own diligence, we will provide cryptographic reference vectors, the source of the timing harness, and a sandbox tenant on the hosted runtime against which the same drills can be run independently. The published SDK is the same primitive used by the hosted runtime, so reviewers can reproduce the cryptographic claims locally with a Node installation.
Run the same drill against your infrastructure
The CLI, the SDK, and the verification suite are open and free. The deniable primitive is consistent across the hosted runtime, the JavaScript SDK, the Rust crate, the Python package, and the Go module. Enterprise deployments add per-tenant isolation, audit logging, and configurable Argon2id parameters; the underlying cryptography is the same code path as the public SDK.
Install the CLI · Enterprise deployment · Read the algorithm in prose