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.0 - April 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

Conventional encryption protects data at rest and in transit. It does not protect against compelled disclosure - scenarios where an adversary can legally or physically compel a key holder to decrypt. In these scenarios, the existence of encrypted data is itself incriminating, and compliance with a decryption demand reveals the protected content.

Deniable encryption addresses this by allowing a single ciphertext to decrypt to different plaintexts under different keys or auxiliary data, with no way for an adversary to determine which decryption produced the “real” plaintext. 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).

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.

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,
C’ Decoy control data
E AES-256-CTR ciphertext
L 4-byte little-endian length prefix

3. Algorithm Specification

3.1 Key Derivation

Two user-supplied passwords (pw1, pw2) are concatenated and passed through scrypt:

combined = pw1 || pw2
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 approximately 2^14 iterations with 8MB memory per derivation, balancing security against browser execution time. Two passwords increase the combined entropy space.

3.2 Encryption

1. Generate random salt S (32 bytes) and IV (16 bytes)
2. Derive key: K = scrypt(pw1 || 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(pw1 || 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.”

  5. 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.

  6. 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:

5.2 What deny.sh Protects Against

Threat Protection
Compelled disclosure with one demand Decoy control file produces plausible alternative
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
Multiple compelled disclosures Subject must have prepared multiple decoys
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 implementation is open source 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 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
deny.sh XOR-composed control data per message Message-level, arbitrary data, unlimited decoys, no special filesystem

8. Verification

All claims in this paper can be independently verified:

  1. Source code: github.com/deny-sh-crypto/deny-sh (AGPL-3.0)
  2. Browser verification suite: deny.sh/verify - 22 tests running live in your browser
  3. Known Answer Tests: Included in all 4 SDK test suites, ensuring cross-implementation consistency
  4. OpenAPI specification: deny.sh/openapi.json for programmatic API validation

9. Future Work

References

  1. Canetti, R., Dwork, C., Naor, M., Ostrovsky, R. (1997). “Deniable Encryption.” CRYPTO ’97.
  2. Percival, C., Josefsson, S. (2016). “The scrypt Password-Based Key Derivation Function.” RFC 7914.
  3. Dworkin, M. (2001). “Recommendation for Block Cipher Modes of Operation.” NIST SP 800-38A.
  4. 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