Cryptographic construction
Part of the Verify pillar. The formal construction behind deny.sh: AES-256-CTR plus scrypt plus XOR composition, with a deniability proof sketch, constant-time notes, and KAT byte-compat figures across four SDKs. Named primitives at every step. No magic.
Draft version: 11 May 2026 · companion to whitepaper v1.2
Why this page exists
The deny.sh primitive is a small piece of cryptography. The novelty is not in any single building block: AES-256-CTR is a NIST-approved cipher with well understood security properties, scrypt is RFC 7914, and XOR is XOR. The novelty is in how the three are composed to produce a ciphertext that can be decrypted to two different plausible plaintexts under two different passwords. This page is the prose companion to that composition, intended for engineers and security reviewers who want the construction in one read without going to the formal whitepaper. The whitepaper remains the canonical specification; this page is a guided tour with the same maths.
The reading order is: the construction in steps, the deniability claim, the security properties we get from each building block, implementation notes on what a sound deployment looks like, and what we explicitly do not claim. Anyone evaluating deny.sh for a procurement decision should be able to read this page once and have a complete picture.
The construction in five steps
Encryption takes two passwords (real and decoy), two plaintexts (real and decoy), and produces one ciphertext plus one decoy control file. The decoy control file is sometimes called the "second password's payload anchor" but in practice it is the bytes that, when fed back into decryption with the decoy password, recover the decoy plaintext. Both files together are the deny.sh artefact.
- Salt and IV: generate a 32-byte cryptographically random salt
Sand a 16-byte AES IV. Both are written into the ciphertext header in cleartext. This is standard practice for AEAD constructions and does not leak information. - Key derivation: hash each password with SHA-256 (producing a fixed 32-byte digest), concatenate the two digests, and feed the 64-byte concatenation through scrypt with parameters
N=2^14, r=8, p=1, dkLen=32. The output is a single 256-bit keyK. Pre-hashing with SHA-256 fixes the input length and prevents the length-extension ambiguity where (abc,def) and (abcd,ef) would otherwise concatenate to the same byte string. - Payload encoding: encode the real plaintext
Pas a 4-byte little-endian length prefix followed by the plaintext bytes:payload = LE32(|P|) || P. The length prefix is needed at decryption time because the XOR composition pads to a fixed envelope. - Control data and XOR: generate random control data
Cof length|payload|, then computeX = payload XOR C. The bytesXare now a pseudorandom intermediate value bound to the real payload. - Cipher pass: encrypt
Xunder AES-256-CTR using keyKand IV:E = AES-256-CTR(K, IV, X). The output ciphertext isS || IV || E. The decoy control dataCis the second output file.
Decryption is the inverse: read S and IV, derive K from the supplied passwords, AES-decrypt to recover X, then XOR with the supplied control data to recover the payload, then strip the length prefix to recover the plaintext. A wrong password produces a different K, which produces a different X, which under XOR with the same control data produces a different payload. The length prefix gives a clean failure signal if the decryption is incoherent.
The deny operation
The deniability comes from a second operation: given an existing ciphertext, the original passwords, and a desired decoy plaintext P', the operator can compute a new control file C' that decrypts to P'. The procedure is:
- Parse the ciphertext, derive
K, AES-decrypt to recover the intermediate valueX(identical to the encryption-timeX). - Encode the decoy plaintext as
payload' = LE32(|P'|) || P', padded to|X|with random bytes if needed. - Compute
C' = X XOR payload'. - Output
C'. The original ciphertext is unchanged.
The critical property: C' is computed directly from X and the desired decoy. No information about the original control data C is needed or revealed. An adversary handed both C and C' alongside the ciphertext cannot determine which was generated first. This is the deniability claim, and it is a property of the XOR algebra, not of any heavyweight cryptographic machinery.
The deniability proof sketch
The formal claim is: given a ciphertext E, two passwords, and two control files C and C' producing plaintexts P and P' respectively, an adversary with access to (E, C, C', P, P') cannot determine which of (C, P) or (C', P') was generated first. The proof sketch:
- Both control files relate to the ciphertext through the same intermediate value
Xby the XOR algebra:P = X XOR CandP' = X XOR C'. Equivalently,C XOR C' = P XOR P'. - The intermediate value
Xis the output of AES-256-CTR under a key derived from two passwords. Under standard cryptographic assumptions (AES-CTR is IND$-CPA secure with a uniformly random key, scrypt is a secure key-derivation function),Xis computationally indistinguishable from a uniformly random byte string of the same length. - Given a uniformly random
X, the relationship between any specific payload and its corresponding control data is purely algebraic XOR. The XOR operation is symmetric and reversible. There is no cryptographic binding betweenXand any particular control-data-to-payload mapping. - Therefore, from the adversary's view, both
(C, P)and(C', P')are equally valid derivations from the same ciphertext. The only distinguishing signal between the pair is whether the recovered plaintext is "the real one". That is a human judgement about the plausibility of the content, not a cryptographic property of the artefact.
The construction does not require the heavyweight machinery used in Canetti et al.'s 1997 paper, which formalised deniability for public-key settings under adaptive adversaries. The symmetric, single-shot setting deny.sh targets (encrypt once, generate one or more decoys, never re-encrypt under the same key) is strictly easier. The full whitepaper at /whitepaper sets out the symmetric reduction and the assumptions in detail. We do not claim a stronger result than the symmetric construction gives us.
Security properties of each building block
Each piece of the construction was chosen for a specific reason. None of them are exotic, and every one is implementable in any language with a standard cryptography library.
AES-256-CTR
The cipher is AES-256 in Counter (CTR) mode. CTR mode turns a block cipher into a stream cipher by encrypting a counter and XORing the output against the plaintext. We use it for two reasons. First, AES-256 is FIPS-approved, has thirty years of cryptanalytic scrutiny behind it, and is hardware-accelerated on every modern CPU. Second, CTR mode is the cleanest block-cipher mode for our needs: it produces a ciphertext exactly the same length as the plaintext (no padding overhead), and the IV-plus-counter structure is well understood. We do not use AES-GCM at this layer, because the authentication tag is supplied by a separate per-payload binding (the length prefix decode plus the calling application's authentication check). AES-GCM would compose differently with the XOR step and complicate the deniability proof sketch above.
scrypt at N=2^14, r=8, p=1
The key-derivation function is scrypt, parameterised at N=2^14, r=8, p=1. These parameters give approximately 16384 iterations with about 8 MB of memory per derivation. They are chosen to be tractable in a browser on commodity hardware (sub-second on a 2022-era laptop without WebAssembly acceleration), while still imposing a meaningful cost on a brute-force attacker. At today's scrypt-cracking rates for these parameters, an attacker faces roughly $6 per guess on commodity GPU hardware. For comparison, a four-word English passphrase has about 44 bits of entropy, which at $6 per guess costs in the tens of millions of dollars to brute-force, and a six-word passphrase pushes that into the trillions. We chose scrypt over Argon2id, despite Argon2id being the 2024 OWASP recommendation, for three concrete reasons documented at /security-posture: cross-SDK byte compatibility (every reference SDK ships with scrypt; Argon2 implementations diverge subtly across language ecosystems), browser performance (scrypt has predictable timing in JavaScript with no WebAssembly fallback complexity), and the maturity of RFC 7914. An archival preset at N=2^17, which raises the per-guess cost into the hundreds of dollars, is on the post-launch roadmap.
XOR composition
The XOR step is the deniability primitive. It is also the simplest piece. The security argument for the XOR is that it provides perfect secrecy when one of the operands is uniformly random (the one-time pad result). The intermediate value X in the deny.sh construction is computationally indistinguishable from uniformly random, by the IND$-CPA security of AES-CTR under a random key. So the XOR step inherits the indistinguishability of X, and the relationship between control data and payload is statistically untraceable without the right key. This is the same algebraic move used in deniable-encryption constructions going back to the 1990s and is robust to all known cryptanalytic attacks on the building blocks.
Length prefix and integrity
The 4-byte little-endian length prefix on the payload serves two purposes. It tells the decryption code where the plaintext ends and any padding begins. And it acts as a per-payload integrity signal: a wrong password produces an essentially random X, which XORed with the control data produces an essentially random payload, which is overwhelmingly unlikely to decode to a valid length prefix followed by valid plaintext bytes. The decryption code rejects payloads whose length prefix exceeds the envelope size or whose recovered plaintext fails the application's own integrity check. The deny.sh primitive does not claim AEAD-style authenticated encryption at the protocol level; that responsibility sits with the calling application, which knows what "valid plaintext" looks like in its own domain. We document the recommended pattern (a short magic-bytes prefix on the plaintext, plus a SHA-256 fingerprint that the caller verifies on decryption) in the SDK documentation at /docs.
Implementation notes
A correct implementation of the construction above gets the deniability claim for free. The interesting work is in not introducing side channels. Notes on what we do and what we recommend reviewers check.
Constant-time operation
The three cryptographic operations in the construction have different constant-time profiles. AES-256-CTR, implemented via the platform's native AES (WebCrypto in browsers, the standard library in Node and Rust and Go), is constant-time at the cipher level on modern CPUs with AES-NI. The XOR operation is constant-time by definition. scrypt is intentionally memory-hard, which means its timing is dominated by memory accesses and is functionally constant with respect to the password (any timing variation is independent of the secret input). The deny.sh SDKs do not perform any password-conditional branching during key derivation or decryption. The implementation notes at /security-posture document the per-SDK approach. Where the host platform offers no constant-time scrypt (very rare), the SDK falls back to a published reference implementation rather than rolling its own.
Memory hygiene
The real plaintext exists in process memory between decryption and use. The SDKs zero out plaintext buffers where the host runtime supports it (TypedArray .fill(0) in browsers, zeroize crate in Rust, manual zeroisation in Go and Node). Garbage-collected runtimes (JavaScript, Go) cannot guarantee that the zeroed buffer was the only copy, so we recommend keeping the decrypted material in a tight scope and not holding it in long-lived process state. The CLI and MCP server use short-lived sub-processes for decryption operations by default to limit the exposure window.
Side-channel resistance
The construction is resistant to common side channels that affect block-cipher implementations (cache-timing, branch-prediction). It is not resistant to host-level side channels that are outside the cipher's control: process memory inspection, keyloggers reading the password as it is typed, microphones detecting keystroke acoustics. These limits are set out at /threat-model.
Cross-implementation byte compatibility
Every reference SDK (TypeScript, Rust, Go, Python) produces byte-identical ciphertexts from the same input parameters. The TypeScript SDK ships with 262 known-answer tests, Rust with 26, Go with 3. The KATs include explicit fixtures for the deny operation (decoy control data generation), proving that a single ciphertext decrypts to multiple known plaintexts under different control files. A third-party implementation that passes the published KATs is fully interoperable with the official SDKs. The KAT fixtures are at github.com/deny-sh-crypto in each SDK's tests/ directory.
Browser-supply-chain hardening
The browser SDK is delivered as a single 8.4 KB minified module from deny.sh's static origin. The served bundle is integrity-pinned with Subresource Integrity (SRI) hashes that are published in the changelog at every release. The build pipeline is reproducible from the public source: a verifier at /verify takes the served bundle, re-derives the SHA-256 hash, and compares it to the latest released SRI hash. A successful match is structural evidence that the served bytes are the bytes that were built from the open-source repository. The Content Security Policy on deny.sh restricts script sources tightly; the post-launch hardening plan moves script-src off 'unsafe-inline' via per-request nonces, closing the residual gap.
What we explicitly do not claim
This is a small primitive, and we are not going to oversell it. The honest list of things this page does not cover:
- A third-party formal cryptographic audit. We have not yet engaged an external cryptographer for a paid review. Outreach for paid informal reviews begins post-launch. Until then, the public artefacts are: this page, the whitepaper, the open-source SDKs (Apache 2.0), the published KAT vectors, the SRI hashes, the reproducible-build verifier at /verify, and the coordinated disclosure policy at /disclosure.
- Authentication against active adversaries at the protocol level. AEAD-style integrity is the calling application's responsibility. The deny.sh primitive itself produces ciphertexts that an active attacker could substitute, replay, or replace. Applications that need active-adversary resistance should layer a signature scheme, MAC, or HTTPS-bound transport on top.
- Quantum resistance. AES-256 has roughly 128-bit security against Grover's algorithm. That is well above current cryptanalytic reach but does not make the primitive post-quantum. We track NIST post-quantum standardisation and will publish a position when a transition is warranted.
- Defence against operator coercion. Cryptographic deniability is a property of the artefact. Operational deniability is a property of how the operator behaves under pressure. The primitive does not promise the second. See /threat-model for the explicit list of scenarios.
References
- Canetti, R., Dwork, C., Naor, M., and Ostrovsky, R. (1997). Deniable Encryption. Advances in Cryptology, CRYPTO '97. The foundational formalisation; deny.sh implements the symmetric one-shot subset.
- Percival, C. (2009). Stronger Key Derivation via Sequential Memory-Hard Functions. The original scrypt paper. RFC 7914 codifies the construction.
- NIST SP 800-38A. Recommendation for Block Cipher Modes of Operation. Specifies CTR mode.
- NIST FIPS 197. Advanced Encryption Standard. The AES specification.
- Bellare, M., and Rogaway, P. (2005). Introduction to Modern Cryptography. Standard reference for the IND$-CPA security model used in the deniability proof sketch above.
- Howard, M., and Lipner, S. (2006). The Security Development Lifecycle. Background on the side-channel and implementation considerations referenced above.
Related pages
- /whitepaper: canonical specification (v1.2, May 2026) with the full algorithm, security analysis, and threat-model treatment in formal language.
- /threat-model: what the primitive defends against, what it does not, and why the agent use case is the strongest fit.
- /security-posture: operational security controls, infrastructure, jurisdiction, and licensing.
- /disclosure: coordinated security disclosure policy.
- /verify: reproducible-build verifier for the browser SDK bundle.
Talk to us
If you are reviewing the construction for procurement, integration, or academic interest, write to hello@deny.sh. We respond to construction questions on a same-week basis and to research enquiries on a same-day basis where possible. Suggested corrections to this page or the whitepaper are welcomed and credited in the changelog.
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.