Threat model
Part of the Verify pillar. What deny.sh defends against, what it partially defends against, and what it does not defend against at all. Plain English, no equivocation. Read this before you decide to trust the infrastructure.
Draft version: 11 May 2026
The shape of the problem
A cryptography product that does not say plainly what it protects against, and what it does not, is asking for trust on faith. Silence reads as either ignorance or evasion. Both are worse than a candid answer. So here is the candid answer.
deny.sh is a deniable encryption primitive. One ciphertext, two keys, two plaintexts. The real password reveals the real data. The decoy password reveals a plausible cover. From the outside, the file is statistically indistinguishable from random. Without one of the two passwords, no party can tell which plaintext is the truth, and no party can tell that a second plaintext exists at all. The construction itself is well studied. The notion was formalised by Canetti, Dwork, Naor and Ostrovsky in 1997, and the symmetric AES plus scrypt plus XOR composition used by deny.sh is a conservative implementation of that idea.
That is the primitive. The interesting question is when the primitive helps and when it does not.
What deny.sh defends against
deny.sh is built for adversaries who acquire your ciphertext and then have to decide what to do with it. The three concrete scenarios where the primitive earns its keep are below.
Bulk leak of at-rest ciphertext
Cloud-synced backups, lost laptops, exfiltrated S3 buckets, stolen hard drives, abandoned office machines, forgotten git history, breached password managers. The adversary holds a file. They do not hold you. They have no way to ask for a password except by guessing, and a guess that decrypts to plausible cover is indistinguishable from the correct password. They take the decoy and move on, because there is no reason to think anything else is there. This is the cleanest fit for the primitive and the largest single category of real-world exposure.
Prompt-injected AI agents
This is the use case the rest of the cryptography literature does not yet address. An AI agent that holds a credential is a credential leaking outwards under adversarial pressure. Prompt injection, jailbreaks, malicious tool output, hostile web pages parsed during a task. Any of these can extract whatever the agent is currently holding in its context window. With deny.sh, the agent only ever holds a control file that decrypts to a plausible decoy credential. The real key never enters the agent's context. When the attacker successfully exfiltrates a secret, what they have is a decoy that authenticates against nothing important. The agent is, in cryptographic terms, the adversary in its own threat model, and it cannot suspect anything outside its context window. That is what makes the agent case structurally strong.
Casual forensic review
A device handed in for repair, returned at the end of an employment relationship, inspected at a customs checkpoint by an officer with no specific suspicion, or analysed by a routine forensic tool. The artefact is high-entropy bytes that decrypt cleanly with the password the operator typed. There is no signature that another plaintext exists. The published forensic-detection literature on hidden-volume systems describes a number of side-channel signals (filesystem timestamps, slack space, application metadata) that can suggest the presence of a hidden volume. deny.sh avoids these by design. A deny.sh ciphertext is a single, ordinary, encrypted-looking file. It does not sit inside another volume. It does not require special filesystem behaviour. It looks like every other encrypted blob.
What deny.sh does not defend against
Every cryptographic tool has limits. The product is more useful, not less, when those limits are written down honestly.
A targeted adversary who already suspects deniable encryption is in use
If an adversary believes you specifically are using deny.sh, sees a deny.sh ciphertext on your device, and is willing to make repeated password demands while observing your reactions, the primitive does not promise to save you. You are now in operational-deniability territory, not cryptographic deniability. The construction protects the mathematical fact that two plaintexts coexist. It does not protect against an adversary who is patient, well resourced, and uninterested in accepting any decryption as final.
Compromise of the host operating system or process memory
deny.sh decrypts in the operator's own environment. Browser, CLI, SDK, MCP server. While the real plaintext is being read, it exists in memory. A kernel-level adversary, a malicious browser extension, a compromised package in the dependency tree, or a memory-snapshot attack against a running process can read that plaintext at the moment of use. The primitive protects the file at rest and protects the agent context against prompt-injection exfiltration. It does not promise host integrity that the operating system itself does not promise. Reproducible builds, signed releases, SRI hashes on the served bundle, and a tight dependency surface (zero runtime dependencies in the SDK) are how we narrow this gap. They do not close it.
Coerced disclosure with no ceiling
The XKCD comic is correct. An adversary willing to apply unlimited physical coercion until they get all the plaintexts you know of is not an adversary that any encryption primitive defeats. Some deniable-encryption products pitch themselves at this scenario. deny.sh does not. Operational deniability against a one-shot demand is plausible. Against an adaptive adversary with iterative demands, the operator is the weak link, not the ciphertext. If the threat model is rubber-hose attack, the right tool is jurisdictional separation and physical absence, not a deniable file format.
Targeted side-channel observation of the operator
If the operator is being filmed, keylogged, or microphone-monitored while they decrypt, the primitive does not help. The adversary is reading the password directly. This sounds obvious, and it is, but it is worth saying.
Why the agent use case is the strongest fit
The limits described above mostly involve a thinking adversary outside the file, applying pressure to the operator. The agent case removes that adversary almost entirely.
An AI agent has no theory of mind. It cannot suspect the presence of a second plaintext that is not in its context. It does not refuse to accept a decryption as final and ask for another one. It does not film the operator at the keyboard. It cannot patiently make repeated password demands while observing reactions, because it does not observe the operator at all. The adversary in the agent case is the agent itself, and the agent is bounded by what is in its context window. Put the real key outside that window, and the agent has no path to it. Put a plausible decoy inside that window, and the agent will hand the decoy to any attacker that successfully prompt-injects it. The decoy is what the attacker gets, every time, by construction.
This is also why we lead with agents at launch. Most of the consumer scenarios the primitive supports (encrypted backups, travel-resistant storage, succession planning, secret photography) involve a human operator who can be pressured. The agent scenario does not. The strength of the primitive shows most cleanly there.
The cryptographic primitive at a glance
For the formal construction, see /security. In summary:
- Cipher: AES-256-CTR over each plaintext, with independent key streams derived from independent password-key-derivation chains.
- Key derivation: scrypt at N=2^14, r=8, p=1, with per-payload random salt. An archival preset (N=2^17) is on the post-launch roadmap.
- Composition: ciphertext is the XOR of the two independent keystream-encrypted payloads, padded to a fixed envelope. The XOR is the deniability primitive. Without the correct password, no party can determine which decryption is the truth and no party can determine that a second plaintext exists.
- Verification: per-payload authenticated tag, computed over the payload under its own key. A wrong password fails verification cleanly. There is no false-positive decryption.
- Surface: the SDK is 8.4 KB minified, zero runtime dependencies, browser-compatible. The CLI, Telegram bot, Chrome extension, and MCP server are thin wrappers over the same primitive.
Cross-SDK known-answer tests (TypeScript, Rust, Go) lock the construction across all language clients. The TypeScript reference SDK has 262 passing KAT vectors; Rust has 26; Go has 3. Any third-party implementation that produces matching ciphertexts is byte-compatible with the published reference.
How we narrow the residual risk
The limits above are real. Some of them we close partially. None of them we pretend do not exist.
- Browser-supply-chain: the served bundle has Subresource Integrity hashes, the build is reproducible from the public source, and the CDN edge is fronted by Cloudflare with a tight Content Security Policy. The post-launch hardening plan moves CSP
script-srcoff'unsafe-inline'via per-request nonces. - Dependency surface: the SDK has zero runtime dependencies. The CLI, bot, and server tools carry their own dependency trees but are not loaded by the SDK itself. A bundler that imports the SDK does not pull in any of them.
- Memory exposure at decryption time: we recommend short-lived decryptions in a dedicated process, zeroisation of plaintext buffers where the host runtime supports it, and not holding decrypted material in long-lived process memory.
- Operational deniability: deny.sh ciphertexts are indistinguishable from random bytes at the file level. Operators are responsible for the surrounding operational hygiene (filesystem traces, application metadata, browser history) that no encryption primitive controls. The documentation in /docs covers the most common operational pitfalls.
- Jurisdiction: the operator of the deny.sh hosted API is Treehouse in Valhalla Ltd, a UK private limited company. UK jurisdiction includes the Investigatory Powers Act 2016 and its Technical Capability Notice regime. Our position on what this means in practice, including the structural limits on what a TCN can compel against a client-side primitive, is set out at /security-posture. Customers requiring jurisdictional separation can self-host the SDKs (Apache 2.0, the source is fully open) or self-host the application-layer code (AGPL-3.0).
References and prior work
- Canetti, R., Dwork, C., Naor, M., and Ostrovsky, R. (1997). Deniable Encryption. Advances in Cryptology, CRYPTO '97. The foundational formalisation of deniable encryption.
- Bertoni, G., Daemen, J., Peeters, M., and Van Assche, G. (2012). Sponge functions and the design of SHA-3. Background on the constructions used for authenticated derivation.
- Percival, C. (2009). Stronger Key Derivation via Sequential Memory-Hard Functions. The original scrypt specification (RFC 7914).
- Forensic-detection literature on TrueCrypt and VeraCrypt hidden volumes describes the side-channel signatures of hidden-volume systems. deny.sh avoids these signatures by not using a hidden-volume model; each ciphertext is a single ordinary file.
- Bellare, M., Boldyreva, A., and O'Neill, A. (2007). Deterministic and Efficiently Searchable Encryption. Background on ciphertext-indistinguishability bounds.
The full construction proof-sketch and implementation notes are at /security. The current operational security posture is at /security-posture. Coordinated security disclosure policy is at /disclosure.
If you have a scenario in mind
If you are evaluating deny.sh for a specific threat model and want to talk through whether it is the right fit, write to hello@deny.sh with a one-paragraph description. We will tell you honestly when the primitive is the right tool and when it is not. We do not have a sales incentive to oversell it.