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.
Living document. Last reviewed: 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 passwords, two control files, two plaintexts. The passwords are the same on every decrypt. What swaps is the control file: feed the ciphertext back with the real control file (and the two passwords) and you recover the real data; feed it back with the decoy control file (and the same two passwords) and you recover a plausible cover. From the outside, the ciphertext is statistically indistinguishable from random. Without the passwords, no party can decrypt at all. With the passwords but only one control file, no party can tell that a second plaintext exists. The construction itself is well studied. The notion was formalised by Canetti, Dwork, Naor and Ostrovsky in 1997, and the symmetric AES plus Argon2id 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 (one runtime dependency in the SDK, hash-wasm, audited and pure-WebAssembly) 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.
Multi-try Honey Mode distinguishers
With Honey Mode, N wrong passwords yield N coherent fake plaintexts. That is useful for single-shot coercion and automated exfiltration, but it also means an adversary who can keep trying wrong passwords can learn that Honey Mode is enabled and infer that a real secret exists. That iterative or repeated-attempt adversary is out of scope. The guarantee is for one-shot disclosure pressure and agents or malware that exfiltrate the first plausible value they recover.
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 under a single 256-bit key K, applied to the XOR intermediate (not to the plaintext directly).
- Key derivation: SHA-256 prehash of each password, concatenated, fed through Argon2id (t=3, m=64 MiB, p=1, v=0x13) into one 256-bit key K, with per-payload random salt. An archival preset (N=2^17) is on the post-launch roadmap.
- Composition: the padded payload is XORed with random control data to produce the intermediate X, then X is encrypted under AES-256-CTR. Swapping the control file is what swaps the plaintext. 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.
- Integrity check: the construction is intentionally unauthenticated (a real AEAD tag would break the deniability property because the presence-or-absence of a verifying tag is itself detectable). Integrity rests on a 4-byte length prefix inside the encrypted zone, plus the calling application's own format check on the recovered plaintext. A wrong password produces a payload whose length prefix and content overwhelmingly fail both checks. See /security for the construction in detail.
- Surface: the TypeScript SDK core is about 3.3 KB minified (1.2 KB gzipped), browser-compatible, with one runtime dependency (
hash-wasmfor portable Argon2id). The CLI, Telegram bot, Chrome extension, and MCP server are thin wrappers over the same primitive.
Cross-SDK known-answer tests lock the construction across all four published SDKs (TypeScript, Rust, Go, Python). Each ships the same three KAT vectors (KAT1, KAT2, KAT3) covering deriveKey on two fixed salts and a full encrypt cycle that is byte-exact across languages. Any third-party implementation that matches all three KAT vectors is byte-compatible with the published reference.
Confidentiality and deniability, not integrity
deny.sh uses AES-256-CTR without a message authentication code. This is intentional: any MAC that verifies is itself detectable, which would let an attacker distinguish the real plaintext from a decoy. There is no known construction that provides strong deniability and strong integrity with a single symmetric primitive. Understand what this means for your use case.
- Passive attacker (observes ciphertext only): you are fully protected. The ciphertext is indistinguishable from random bytes. No information about the real plaintext, the decoy, or the passwords leaks from inspection of the file.
- Active attacker (can modify the ciphertext in transit or at rest): AES-CTR is malleable. An attacker who knows the plaintext at a specific offset can flip bits at that offset in the ciphertext to flip the same bits in the decrypted plaintext. The decryption will succeed silently, and you will read the modified content without any error or indication. This is not a bug in the implementation; it is a structural property of any unauthenticated stream cipher.
- Mitigation: if your threat model includes an active attacker who can modify stored or in-transit ciphertexts, layer an external integrity check. Sign or MAC the ciphertext with a key that remains under your control. The signature key selection determines who can verify. The audit chain at /dashboard/audit provides RFC 3161 timestamps and signed receipts on the hosted service for exactly this purpose.
- Most deniability use cases do not require integrity: coercion resistance, plausible deniability under legal compulsion, and whistleblower-drop scenarios are primarily concerned with forced disclosure, not in-flight tampering. If the threat is an adversary demanding your passwords, not an adversary who has already modified your files, the lack of a MAC is not a meaningful gap.
What encryptRecord protects and what it does not
The encryptRecord API encrypts structured key-value records and generates a decoy record whose values are realistic fakes. This provides value-deniability: the decoy record has plausible-looking field values rather than the real ones. It does not provide schema-deniability.
Specifically: the decoy frame uses the same field names and type classes as the real frame, because realistic decoys require structural similarity. An attacker who coerces a successful decoy decrypt learns the field names and type classes of the real record (for example, "this record has a field named api_key whose value is a Stripe live key"), even though the actual value is a decoy. Only the values are protected, not the schema.
If full schema-deniability is required, encrypt the entire record as a freeform blob using the base encrypt() function with a hand-crafted decoy of the same byte length. This hides field names, field count, and type class, at the cost of manual decoy construction.
Decoy realism and context-distinguishability
deny.sh decoys are not just structurally realistic, they are deep-valid: the decoy engine regenerates each candidate until it passes the same integrity check a real value would. A credit-card decoy passes the Luhn checksum, a seed-phrase decoy has a valid BIP-39 checksum, an IBAN decoy satisfies ISO 7064 mod-97, a Bitcoin WIF key passes Base58Check, and so on. An adversary who runs each candidate through a provider's verification surface (Luhn, BIP-39 checksum tool, IBAN check-digit library) gets a pass on both the real value and the decoy. The checksum is no longer a distinguisher. See /how-it-works for the full distinguisher analysis.
What deep validity cannot do is judge context. The maths makes a decoy checksum-valid and the right size; it cannot decide 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. This is the one distinguisher we cannot close for you, and we do not pretend to.
The consequence for your threat model: choosing a plausible-in-context decoy is the human half of deniability and it stays with you. A decoy that is perfectly checksum-valid but obviously out of place (an empty wallet next to evidence you trade daily, a decoy note that contradicts your situation) can still be distinguished by an adversary who reasons about context rather than just running a validator. For high-stakes field types, hand-craft the decoy with the base encrypt() API so it is both deep-valid and contextually believable.
Distributional realism is a separate axis from checksum-validity, and we do not claim to have closed it at corpus scale. A real provider credential carries distributional signatures beyond its checksum: prefix conventions, character-class ratios, entropy profile, embedded version or region markers. A validator-gated decoy is structurally indistinguishable on the checks a provider's own verification surface runs, but an adversary who specifically suspects deny.sh, holds a large corpus of genuine values for that credential type, and runs a statistical classifier may be able to separate generated decoys from reals at better than chance. This matters only for that specific, well-resourced adversary, not for the single-decoy-walk-away threat the primitive is designed around. Narrowing it is active work on the realism engine; we will not overclaim statistical indistinguishability we have not measured.
What we don't hide (metadata that still leaks)
Deniable encryption operates on the contents of a ciphertext. It is not invisibility. A class of side-channels remains observable to anyone with the right vantage point, and we name them here rather than make you discover them.
- Existence of a ciphertext. A deny.sh ciphertext is indistinguishable from random bytes at the file level, but the fact that a file exists in a particular place at a particular time is visible to anyone with filesystem, backup, or storage-system access. The decoy story addresses what the bytes say. It does not address that the bytes are there.
- Ciphertext length. Each ciphertext is padded to a fixed envelope and the real plaintext is bounded by the envelope size. The envelope size itself is observable to anyone holding the bytes. It does not reveal which of two plaintexts is the real one, but it does establish an upper bound on the real plaintext length, and that bound constrains the plausibility of future decoys against the same envelope.
- Hosted-API exposure. The hosted endpoints at
api.deny.share an opt-in convenience surface. Two things are exposed by using them. First, plaintext and passwords pass through our server in memory during the call:POST /api/encrypt,/api/decrypt, and/api/denyaccept plaintext and passwords in the request body, the server performs the construction in RAM, returns the result, and does not persist plaintext or passwords. This is a deliberate convenience tradeoff, not a zero-knowledge surface. Second, per the data-processing-addendum at /dpa, our server logs and rate-limit state capture request timestamps, request counts, response codes, source IP, request path, and user agent against the calling API key. If either of these is part of your threat model, use the browser tools, the CLI, or the SDK in its default offline mode instead. The three local MCP tools and the offline SDKs run the identical construction on your machine with no network calls during local crypto and leak neither plaintext nor metadata to us. Ciphertext is byte-identical across hosted and local paths; pick once, swap later by changing one import. Full surface-by-surface breakdown at /what-we-see. - Timing and frequency. The shape of when and how often you encrypt is visible on any hosted surface (rate-limit state, audit log on the Agents Infrastructure tier) and visible at the network level on any client-side surface (TLS connection timing). Bursty encryption activity, predictable cadences, or correlated bursts across keys are observable side-channels even when no plaintext leaks. The construction does not flatten timing on its own.
- Network identifiers. Any HTTPS call you make to
api.deny.shexposes source IP and TLS metadata to us and to anything in the network path. Tor, a VPN, or a self-hosted deployment removes us from the trust boundary for those identifiers; nothing in the construction does. - Operational filesystem traces. Browser history, temp files, swap pages, application logs, autosave artefacts, OS thumbnail caches, and cloud-backup snapshots are outside the cipher's reach. Operators are responsible for the surrounding hygiene; the construction protects what's inside the ciphertext, not what the host operating system writes around it.
- Account state. Hosted API keys, billing records (handled by Stripe, never by us in raw form), audit-log retention (tier-specific, documented at /compliance), and the existence of an active key under a given email are visible to us as the operator of the hosted plane. Self-hosting eliminates this exposure.
None of the above is a flaw in the construction. They are the boundary between what an encryption primitive controls and what it does not. We document them here so that anyone evaluating deny.sh for a specific scenario can decide whether the boundary is in the right place for them.
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 TypeScript SDK has one runtime dependency,
hash-wasm, which provides the portable Argon2id implementation.hash-wasmis a pure-WebAssembly module with no further runtime dependencies of its own and is published as a single audited bundle. The CLI, Telegram bot, Chrome extension, and MCP server carry their own dependency trees but are not loaded by the SDK itself. A bundler that imports just the SDK pulls inhash-wasmand nothing else. - 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).
Defences in production today
The five engineering controls live on the hosted service today. Each one closes a specific threat from the lists above. We name the mapping so a reviewer can see the response to each modelled adversary capability, not just the capability itself.
- Audit chain with signed receipts (RFC 3161 timestamped, verifiable without trusting deny.sh): closes the silent-tampering and silent-replay failure modes for any operation that touches the hosted runtime. A regulator or auditor can verify that an operation occurred from the chain alone. Dashboard at /dashboard/audit.
- Durable usage metering: closes the silent-overage and credentials-leaked-and-burned-without-notice failure modes by emitting 80 / 95 / 100 percent quota alerts to the customer.
- BYOK envelope (AWS KMS): closes the operator-can-decrypt-server-stored-ciphertext concern. Per-record AES-256-GCM DEKs wrapped under the customer's KMS CMK. Customer revokes the CMK and the server-stored copy goes dark; we cannot decrypt it.
- Pre-built integrations: closes the regulated-counterparty integration burden. AWS Secrets Manager custodian gives the customer a third-party-verifiable copy of critical encrypted material; signed outbound webhooks route critical events to the customer's incident channel; SAML SSO with replay protection and email domain allowlist defends the workforce-identity boundary.
- Compliance umbrella: closes the procurement-evidence gap by publishing the controls map and operational status. See Trust Center and /compliance#controls-map.
What the moats deliberately do NOT close: the cryptographic primitive itself (covered by the construction at /security), operational deniability outside the hosted runtime (see the operational deniability bullet above), and the residual jurisdictional risk (see the jurisdiction bullet above). The moats are layered defences around the primitive, not a substitute for it.
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.
- Biryukov, A., Dinu, D., Khovratovich, D. (2021). Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications. RFC 9106.
- 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.