Threat model
For security and procurement review: what the design protects. Check the conditions needed for protection, the limits that remain and the evidence your deployment requires.
Living document. Last reviewed: 11 May 2026.
Start with your risk
Use this page to decide whether the design fits your risks. Deniability offers a chosen alternative message; it does not make compromised systems safe or guarantee that another person accepts the alternative.
For browser and core code libraries (SDKs), one encrypted file (ciphertext) opens to two messages (plaintexts). Both passwords are always required together. The control file, a separate file you keep safe, selects the real message or a chosen fake (decoy). A wrong password/control combination returns plausible garbage without an error; Honey Mode returns a fake in a supported format instead. The Telegram bot and protect-seed wizard differ: each control file is bound to its own password. The combination used here awaits independent cryptographic review.
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
If someone obtains only encrypted data, strong passwords and separate control files remain important. A person given both passwords and only the decoy control file can open the chosen decoy. Wrong guesses produce garbage, or supported-format fakes with Honey Mode. External evidence may identify a fake; there is no guarantee that an attacker stops searching.
Prompt-injected AI agents
Prompt injection is an attempt to make an agent follow hostile instructions in material it reads. If your integration gives the agent only a decoy or reference, it cannot extract a real key from that material. Keep the real control file, passwords and resolved key inside trusted software. The agent may still misuse an authorised tool, and a compromised runtime may expose the key.
Casual forensic review
A decoy can provide an alternative message when a file is inspected. This does not hide use of deny.sh, filesystem metadata, backups, logs or other evidence. A single encrypted file avoids the need for a hidden volume, but it does not guarantee that forensic analysis cannot distinguish the messages.
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. Keeping real keys outside agent-accessible paths can limit exposure from its context. 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 (including the browser dependency hash-wasm, a WebAssembly module) are how we narrow this gap. They do not close it.
Coerced disclosure with no ceiling
Encryption does not protect a person from coercion. A decoy is not a guarantee of safety, acceptance or compliance with a disclosure demand. Assess personal, legal and operational risks separately.
Multi-try Honey Mode distinguishers
Honey Mode returns deterministic fakes for wrong password/control combinations: the same wrong inputs give the same fake. Repeated guesses, known real values or checks against the provider may distinguish those fakes. It does not guarantee protection against a repeated-attempt adversary or physical coercion.
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.
What makes an agent integration useful
Agents are a useful integration case because you can control which tools and credentials they can access. The protection comes from enforced access restrictions, not assumptions about how the model reasons.
Keep the real key outside the model’s conversation, tools, logs and readable files. Let trusted code use it and return only approved results. A decoy inside the agent’s context can be exposed instead, but only if every route to the real credential remains restricted. Test this boundary and authorised-tool misuse separately.
The same design may suit private files, but the surrounding risks differ. Decide by where the real control material can be kept and what evidence an adversary can obtain, rather than by the presence of a decoy alone.
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
The core scheme uses AES-256-CTR without a message authentication code (MAC), a check for unauthorised changes. A wrong password/control combination returns plausible garbage without an error, or a supported-format fake with Honey Mode. If your application needs integrity checks, review how those checks affect deniability and avoid exposing a real-versus-decoy signal.
- 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
Supported decoys are checked for format and checksum validity, called deep-valid: the generator checks supported formatting rules before returning a candidate. 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 local format check (Luhn, BIP-39 checksum tool, IBAN check-digit library) gets a pass on both the real value and the decoy. A passing checksum is not proof that a credential is live or indistinguishable from a real one. 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. Provider validation and other external evidence can also identify a fake.
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 format-checked decoy passes the supported local checks, not necessarily provider validation, 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 is another reason not to assume an attacker will accept a decoy. 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 and account audit history) 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.
These are limits to evaluate, not evidence that every deployment is safe. Decide whether the remaining exposure meets your requirements.
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 /trust. 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
Hosted controls add key custody, activity records and notifications. They reduce specific operational risks but do not remove the limits of the encryption design.
- 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 Centre and /compliance#controls-map.
What the controls 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 controls 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 /trust. 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. Include the data involved, who can access it and which systems must remain outside the trust boundary.