Whitepaper
The cryptographic specification behind deny.sh: XOR-composed control data, AES-256-CTR, and provable deniability.
deny.sh: Deniable Encryption via XOR-Composed Control Data
Version 1.2 - May 2026 deny.sh
Abstract
deny.sh implements deniable encryption using XOR composition over AES-256-CTR ciphertext with scrypt-derived keys. A single ciphertext can be decrypted to arbitrarily many distinct plaintexts, each producing valid output, with no cryptographic marker distinguishing the "real" decryption from any decoy. This paper specifies the algorithm, analyses its security properties, defines the threat model, and presents empirical verification results.
1. Introduction
This paper specifies the cryptographic primitive at the core of deny.sh and analyses its security properties. The primitive is the smallest part of the product. The product itself is what we call deniability infrastructure: the operational layer that makes the primitive usable on behalf of others, at multi-tenant scale, with a verifiable trust posture. This section defines both the primitive and the surrounding category claim, in that order, so that the technical sections that follow can be read either as a standalone cryptographic specification or in their wider product context.
1.1. The problem
Conventional encryption protects data at rest and in transit from adversaries who do not hold the key. It does not protect the bytes once they leave the holder's control: cloud-storage breaches, stolen or seized devices, exfiltrated backups, accidental publication of source repositories, and AI agents whose execution context is compromised by prompt injection all produce a single failure mode - ciphertext in adversary hands, where the only remaining defence is the strength of the password and the unwillingness of the adversary to keep trying. The existence of the encrypted file frequently reveals more than the file's contents do.
Deniable encryption addresses this gap by allowing a single ciphertext to decrypt to different plaintexts under different keys or auxiliary data, with no way for an adversary - even one with complete forensic access to the leaked artefacts - to determine which decryption produced the “real” plaintext. When the bytes leak, the adversary's recovered plaintext is, by construction, indistinguishable from a decoy. The concept was formalised by Canetti et al. (1997) and implemented in various forms by TrueCrypt/VeraCrypt (hidden volumes) and Off-the-Record Messaging (deniable authentication).
1.2. The primitive
deny.sh takes a different approach: rather than hidden volumes or deniable key exchange, it uses XOR composition with control data to produce multiple valid decryptions from a single ciphertext. This is simpler, more portable (works on arbitrary data, not just disk volumes), and produces deniability at the message level rather than the container level. The construction is given in full in Section 3 and analysed in Section 4.
1.3. The infrastructure claim
The primitive alone is sufficient for an individual encrypting their own data on their own machine. It is not sufficient for the audiences deny.sh is built for, which include AI agent platforms holding tenant secrets at scale, custody products with hot-wallet credentials, and regulated organisations whose audit trail must accommodate deniable at-rest storage. For these audiences, the operational requirements exceed what any library provides: per-tenant key isolation that is cryptographic rather than policy-based; structured audit logging with defined retention; coordinated vulnerability disclosure with a real fingerprint and a real human at the other end; reproducible build verification; and a continuity story that survives planned and unplanned outages of the hosted service.
We therefore present deny.sh as deniability infrastructure, organised around three pillars:
- Encrypt. The cryptographic primitive specified in Sections 2-4 of this paper. Shipped as an Apache 2.0 SDK in TypeScript, Rust, Go, and Python.
- Operate. The hosted runtime: per-tenant isolation, structured 365-day audit log, MCP server, named SLA, and local-mode fallback for continuity. Available as a managed service and as a private deployment under the AGPL-3.0 application layer or a commercial licence.
- Verify. The published trust posture: this paper,
the formal construction write-up at
/security, the plain-language threat model at/threat-model, the coordinated disclosure policy at/disclosure, and the reproducible-build manifest at/.well-known/integrity.json. An independent cryptographic audit is in scoping for Q3 2026.
The remainder of this paper concerns Pillar 1, the cryptographic primitive, in detail sufficient for independent implementation and review. Operational and trust-posture material is referenced where relevant and lives in full on the product surfaces above.
2. Notation
| Symbol | Definition |
|---|---|
| P | Plaintext (arbitrary bytes) |
| P' | Decoy plaintext |
| K | Encryption key (32 bytes, derived from passwords) |
| S | Salt (32 bytes, random) |
| IV | Initialisation vector (16 bytes, random) |
| C | Control data (random bytes, length at least |P| + 4) |
| C' | Decoy control data |
| E | AES-256-CTR ciphertext |
| L | 4-byte little-endian length prefix |
3. Algorithm Specification
3.1 Key Derivation
Each user-supplied password is first hashed with SHA-256 to produce a fixed-length 32-byte digest. The two digests are concatenated and passed through scrypt:
pw1_hash = SHA-256(pw1) (32 bytes)
pw2_hash = SHA-256(pw2) (32 bytes)
combined = pw1_hash || pw2_hash (64 bytes, fixed-length)
K = scrypt(combined, S, N=16384, r=8, p=1, dkLen=32)
The salt S is 32 bytes of cryptographically random data, generated per encryption operation and prepended to the output.
Rationale. scrypt provides memory-hard key derivation, making brute-force attacks on weak passwords computationally expensive. The parameters (N=16384, r=8, p=1) provide 2^14 iterations with 8 MB memory per derivation, balancing security against browser execution time on commodity hardware (the SDKs target sub-second derivation in browsers without hardware acceleration). Two passwords increase the combined entropy space.
SHA-256 is applied to each password before concatenation so the
scrypt input has a fixed 64-byte structure regardless of password
length. This eliminates length-extension ambiguity at the password
boundary: without the pre-hash, pairs such as (abc,
def) and (abcd, ef) would
concatenate to the identical byte string abcdef and derive
the identical key. SHA-256 is a fast cryptographic hash; the pre-hash
adds negligible cost relative to the scrypt invocation that follows. All
four reference SDKs (TypeScript, Python, Rust, Go) implement this
binding identically and produce byte-compatible ciphertext.
3.2 Encryption
1. Generate random salt S (32 bytes) and IV (16 bytes)
2. Derive key: K = scrypt(SHA-256(pw1) || SHA-256(pw2), S)
3. Encode payload: payload = LE32(|P|) || P
4. Generate random control data: C ← random(|payload|)
5. XOR: X = payload ⊕ C
6. Encrypt: E = AES-256-CTR(K, IV, X)
7. Output: ciphertext = S || IV || E
8. Output: controlData = C
The 4-byte little-endian length prefix (LE32) encodes the plaintext length, enabling correct extraction during decryption when the control data may be longer than the plaintext.
3.3 Decryption
1. Parse: S = ciphertext[0:32], IV = ciphertext[32:48], E = ciphertext[48:]
2. Derive key: K = scrypt(SHA-256(pw1) || SHA-256(pw2), S)
3. Decrypt: X = AES-256-CTR-Decrypt(K, IV, E)
4. XOR with control data: payload = X ⊕ C
5. Extract length: len = LE32(payload[0:4])
6. Extract plaintext: P = payload[4 : 4+len]
3.4 Deniable Control Data Generation (Deny Operation)
Given a ciphertext, the original passwords, and a decoy plaintext P':
1. Parse and derive K from ciphertext (same as decryption steps 1-2)
2. Decrypt: X = AES-256-CTR-Decrypt(K, IV, E)
3. Encode decoy payload: payload' = LE32(|P'|) || P'
4. Pad payload' to |X| with random bytes if |payload'| < |X|
5. Compute decoy control data: C' = X ⊕ payload'
6. Output: C'
Critical property: C' is computed directly from the decrypted intermediate value X and the desired decoy payload. No information about the original control data C is required or revealed. Multiple decoy control files can be generated from a single ciphertext, each producing a different plaintext.
3.5 Ciphertext Format
Offset Length Field
0 32 Salt (random)
32 16 IV (random)
48 var AES-256-CTR encrypted data
Total overhead: 48 bytes. Control data is stored separately.
4. Security Analysis
4.1 Confidentiality
Confidentiality rests on AES-256-CTR, which is IND-CPA secure under standard assumptions. The scrypt KDF provides resistance to brute-force key recovery. The XOR composition with control data does not weaken AES - it is applied before encryption, not after.
4.2 Deniability
Theorem: Given a ciphertext E and two control files C and C', an adversary with access to the passwords cannot determine which control file was generated first (i.e., which corresponds to the "real" plaintext).
Proof sketch: Let X = AES-256-CTR-Decrypt(K, IV, E). For any valid control data C, the payload is P_C = X ⊕ C. For any desired plaintext P, we can compute C = X ⊕ (LE32(|P|) || P || pad). Since X is fixed for a given ciphertext and key, the relationship between C and P is purely algebraic (XOR). There is no cryptographic binding between a specific C and the "original" encryption operation.
The only information that distinguishes C from C' is: 1. The length prefix must decode to a valid length (trivially satisfied by construction) 2. The plaintext must be "plausible" (a human judgment, not a cryptographic property)
An adversary who obtains both C and C' learns nothing about which was generated first. The XOR operation is symmetric and reversible; both control files are equally valid derivations from the intermediate value X.
4.3 Statistical Indistinguishability
Control data C is generated from random(n) during
encryption. Decoy control data C' = X ⊕ payload' where X is AES-CTR
output (pseudorandom) and payload' is structured data. If the adversary
does not know X, C' appears random.
If the adversary DOES know X (i.e., has the passwords), they can derive any control file from any plaintext, but they cannot determine temporal ordering. The statistical properties of C and C' are identical when X is treated as a pseudorandom mask.
Empirical verification (from deny.sh/verify, 22 tests): - Chi-squared uniformity: 99.6% pass rate across 1,000 generated control files - Mean ciphertext entropy: 7.196 bits/byte (theoretical maximum: 8.0) - Kolmogorov-Smirnov statistic: D = 0.1528 (well within random bounds for n=1000) - Serial correlation coefficient: ≈ 0.0 (no detectable sequential correlation) - 500 fuzz rounds with random inputs: 0 failures - 100 distinct decoy control files generated from a single ciphertext: all valid
4.4 Limitations
Known limitations:
Plausibility is not cryptographic. The deniability guarantee is mathematical (any control file produces a valid decryption), but its real-world effectiveness depends on the plausibility of the decoy plaintext. A decoy that reads "this is a decoy" provides no practical deniability.
Rubber-hose resistance is bounded. An adversary who demands additional passwords after receiving a decoy can continue demanding until the subject runs out of prepared decoys. deny.sh does not limit the number of decoy control files, but the subject must have prepared them in advance.
Control file existence is detectable. If an adversary finds multiple control files, they know the subject used deniable encryption. deny.sh does not hide the existence of the control files themselves - steganography (deny.sh/stego-app) can address this.
Password reuse across control files. All control files for a given ciphertext use the same password pair. An adversary who obtains the passwords can generate their own control files for arbitrary plaintexts - but this does not help them determine which existing control file is "real."
Side-channel attacks. deny.sh does not protect against timing analysis, memory forensics, or other side-channel attacks on the client device. The encryption runs in the browser's JavaScript engine, which is not designed for side-channel resistance.
scrypt parameter choice. N=16384 is a conservative parameter that balances browser execution time (typically 200-800ms) against brute-force resistance. For high-value targets, higher parameters may be appropriate. The CLI and SDK support configurable parameters.
5. Threat Model
5.1 Adversary Capabilities
deny.sh is designed to resist the following adversary:
- Has the ciphertext. (Assumed - the file leaked from a cloud-storage breach, was recovered from a stolen or seized device, was exfiltrated from a host, was published accidentally to a public repository, or was disclosed by a compromised AI agent.)
- Has one or more control files. (Assumed - found alongside the ciphertext as part of the same leak, or - in a secondary supported case - obtained from the subject under bounded legal/operational compulsion where the subject pre-positioned a decoy.)
- Has unlimited computational resources for cryptanalysis (but not for scrypt brute-force within practical time bounds).
- Knows the subject uses deny.sh. (Kerckhoffs’s principle - the algorithm is public.)
The construction is not designed to resist an adaptive adversary who already knows the tool is in use in the subject's life and can iteratively demand additional passwords; that scenario is an operational-deniability failure that the cryptographic primitive does not address (see Section 5.3).
5.2 What deny.sh Protects Against
| Threat | Protection |
|---|---|
| Ciphertext leak with one or more control files alongside | Each control file produces a plausible plaintext; bytes resolve to the decoy under any leaked key |
| Cryptanalytic attack on ciphertext | AES-256-CTR with scrypt KDF |
| Statistical analysis of control files | Empirically verified uniformity (Section 4.3) |
| Forensic detection of "real" vs "decoy" | No cryptographic marker exists (Section 4.2) |
5.3 What deny.sh Does Not Protect Against
| Threat | Limitation |
|---|---|
| Adaptive adversary with iterative password demands | Operational-deniability failure; not addressed by the primitive (subject must have pre-positioned every decoy the adversary can request) |
| Physical compromise of the device | Side-channels, memory forensics |
| Adversary with original plaintext | Trivially computable control data |
| Implausible decoy content | Human judgment, not a crypto property |
6. Implementation
6.1 Browser Implementation
The browser tools use the Web Crypto API (AES-CTR) and a JavaScript scrypt implementation. No data leaves the browser. The SDK primitive used by the browser tools is open source under Apache License 2.0; the application-layer browser-tool source (UI shell, vault integrations, dead-man's switch flows) is under AGPL-3.0.
6.2 API Implementation
The server-side API uses Node.js built-in crypto module
(AES-256-CTR) and the scrypt function from
node:crypto. The API receives pre-encrypted data for vault
storage and operates on plaintext only during encrypt/decrypt/deny
operations. API keys are stored as SHA-256 hashes.
6.3 SDK Implementations
SDKs are available in TypeScript, Python, Go, and Rust. All implementations produce identical ciphertext for identical inputs (verified via Known Answer Tests). Cross-language interoperability is guaranteed by the fixed format specification in Section 3.5.
6.4 Chrome Extension
A Manifest V3 Chrome extension provides encrypt/decrypt/vault operations directly in the browser toolbar. The crypto engine is adapted from the browser tools and runs entirely within the extension sandbox. Right-click context menus allow encrypting selected text on any page.
6.5 CLI
An enhanced command-line interface wraps the TypeScript SDK with
commands for encryption, decryption, vault management, environment
variable protection, and Shamir secret splitting. The CLI supports pipe
input, hidden password prompts, and manages a .deny/
directory convention with automatic gitignore management. Zero runtime
dependencies beyond node:crypto.
6.6 Password Manager Integration
Control data can be pushed to and pulled from 1Password (via
op CLI) and Bitwarden (via bw CLI) as Secure
Notes. This allows users to manage control files within their existing
credential workflows without exposing plaintext to the password
manager.
6.7 Cloud Backup
An encrypted backup system creates scrypt + AES-256-CTR archives of
control data for storage on local disk, Google Drive, Dropbox, or Amazon
S3. Archives use a DENY_BACKUP_V1 format with independent
key derivation from the backup passphrase.
6.8 Telegram Bot
A Telegram bot (@denyshbot) provides conversational encrypt/decrypt/vault workflows. Password messages are automatically deleted from chat history after use. Crypto operations are restricted to direct messages.
6.9 MCP Server for AI Agents
A Model Context Protocol server exposes 11 tools (4 local, 7 API) for AI agent integration. Local tools (encrypt, decrypt, deny, Shamir split) operate with zero knowledge and require no API key. This enables AI agents to handle deniable encryption workflows programmatically.
6.10 Dependencies
The TypeScript SDK has zero runtime dependencies (8.4KB, uses only
node:crypto). Browser tools use Web Crypto API with no
external libraries.
7. Comparison with Existing Approaches
| Approach | Deniability Model | Limitations |
|---|---|---|
| VeraCrypt hidden volumes | Hidden OS/partition within encrypted volume | Requires disk-level access, detectable via disk analysis heuristics, limited to one hidden volume |
| OTR Messaging | Deniable authentication (not encryption) | Only applies to message authentication, not content |
| Rubberhose (defunct) | Multiple encrypted partitions with independent keys | Abandoned, complex, filesystem-level only |
| Cryptee ($3-27/mo) | Ghost folders (UI-level hiding) | UI deniability only, not cryptographic. Hidden content is still present in the encrypted store and discoverable with forensic access |
| Stegg (ste.gg) | Steganographic embedding (112 techniques) | Hides data within carrier files but does not encrypt by default. Nested layers possible but no deniable decryption |
| deny.sh | XOR-composed control data per message | Message-level, arbitrary data, unlimited decoys, no special filesystem, cryptographic deniability + optional steganographic embedding |
8. Verification
All claims in this paper can be independently verified:
- Source code: All four reference SDKs are Apache License 2.0 and listed at deny.sh/sdks. The Python, Rust, and Go ports are public mirrors at deny-python, deny-rs, and deny-go. The TypeScript reference SDK is published on npm as deny-sh; its source-code mirror at deny-js becomes public at the 2026-07-04 launch. All four implementations share the cross-SDK Known Answer Tests below.
- Browser verification suite: deny.sh/verify - 22 tests running live in your browser
- Known Answer Tests: Included in all 4 SDK test suites, ensuring cross-implementation consistency
- OpenAPI specification: deny.sh/openapi.json for programmatic API validation
9. Future Work
- Independent third-party cryptographic audit (in scoping for Q3 2026, results published when complete)
- Formal verification of the deniability property using ProVerif or similar tools
- Hardware-accelerated scrypt for higher parameters in browser
- Chrome Web Store publication for the browser extension
- Package registry publication for Python (PyPI), Go, and Rust (crates.io) SDKs
- Inline mode for the Telegram bot (encrypt in any chat via
@denyshbot)
References
- Canetti, R., Dwork, C., Naor, M., Ostrovsky, R. (1997). "Deniable Encryption." CRYPTO '97.
- Percival, C., Josefsson, S. (2016). "The scrypt Password-Based Key Derivation Function." RFC 7914.
- Dworkin, M. (2001). "Recommendation for Block Cipher Modes of Operation." NIST SP 800-38A.
- Boyen, X., Haines, T., Müller, T. (2018). "A Formal Treatment of Deniable Encryption."
deny.sh - deny.sh, UK (Co. 15770209) hello@deny.sh | deny.sh