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.6 - June 2026 deny.sh
Patent Pending: US Provisional Application No. 64/067,717 (filed 17 May 2026)


Abstract

deny.sh is deniability infrastructure for engineering teams that hold other people's secrets. Modern systems concentrate credentials in places that leak: AI agents whose context can be prompt-injected, multi-tenant platforms storing thousands of users' keys, and backups or repositories that escape every perimeter built to contain them. Conventional encryption protects those secrets only until the key is produced; once the bytes and a working key reach an adversary, the recovered plaintext is the real plaintext. deny.sh removes that finality. Its core primitive - XOR composition over AES-256-CTR with Argon2id-derived keys - lets a single ciphertext decrypt to arbitrarily many valid plaintexts, with no cryptographic marker distinguishing the real one from a decoy. Around that primitive it adds the operational layer production systems require: per-tenant cryptographic key isolation, shape- and checksum-correct decoys across 69 credential types, and a hash-chained audit log. This paper specifies the primitive in full, 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

Software increasingly concentrates other people's secrets in places that are one mistake away from disclosure. An AI agent holds API keys, wallet credentials, and tenant tokens in a context window that a single prompt injection can exfiltrate. A multi-tenant platform stores thousands of customers' secrets behind one application boundary, so one breach is everyone's breach. Backups, exported environment files, and accidentally-published repositories carry live credentials straight through every perimeter built to contain them. In each case the failure mode is identical: ciphertext, and often a key that reads it, end up in an adversary's hands.

Conventional encryption - the protection for data at rest and in transit - does nothing past that point. Once the key is produced, whether by brute force, by coercion, or by a compromised agent that simply hands it over, the plaintext it yields is the real plaintext. The existence of the encrypted blob frequently reveals as much as its contents do: that a secret exists, that it was worth protecting, and that whoever holds the key now holds the secret.

Deniable encryption removes that finality. A single ciphertext decrypts 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, even one holding a valid key - to determine which decryption produced the "real" plaintext. What leaks is, by construction, indistinguishable from a decoy. The property was formalised by Canetti et al. (1997); deny.sh's contribution is an implementation that is practical at production scale rather than confined to disk-volume containers (Section 8).

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:

  1. 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.
  2. 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.
  3. 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 on the roadmap.

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 Argon2id:

pw1_hash = SHA-256(pw1)            (32 bytes)
pw2_hash = SHA-256(pw2)            (32 bytes)
combined = pw1_hash || pw2_hash    (64 bytes, fixed-length)
K        = Argon2id(combined, S, t=3, m=64 MiB, p=1, v=0x13, dkLen=32)

The salt S is 32 bytes of cryptographically random data, generated per encryption operation and prepended to the output.

Rationale. Argon2id provides memory-hard key derivation and is OWASP's currently recommended password-based KDF. The parameters (t=3 iterations, m=64 MiB memory, p=1 parallelism, v=0x13) deliberately exceed OWASP's minimum recommendation (t=2, m=19 MiB) to push GPU/ASIC brute-force attacks well above commodity-cracking economics. Memory-hardness makes brute-force scale with RAM cost, not just compute, dramatically raising the per-guess cost for an attacker. Two passwords increase the combined entropy space.

SHA-256 is applied to each password before concatenation so the Argon2id password 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 Argon2id 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 = Argon2id(SHA-256(pw1) || SHA-256(pw2), S, t=3, m=64 MiB, p=1, v=0x13)
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.

Because the length prefix lives inside the XOR-and-encrypt zone, the decryption output never reveals the real plaintext length: a 2-byte decoy and a 64-byte real message can share a single ciphertext, and which one is produced is determined entirely by the control file. This is what makes value-deniability work across differing plaintext lengths.

3.2.1 Length Bucketing (Length Privacy)

The length prefix hides the real length from the decryption output, but the ciphertext byte-count itself is fixed at encryption time. Without padding it equals |P| + 4, so an adversary who only ever observes the ciphertext can read the real plaintext length directly off the wire. For some payloads this is a meaningful leak (it distinguishes a 12-word from a 24-word BIP-39 seed phrase; it bounds the size of a protected message).

To close this channel, the encryptor MAY pad the inner payload with random bytes up to a coarse size band before encryption:

bands = { 64, 256, 1024, 4096, 16384 } bytes   (payloads above 16384 round up to the next 16 KiB multiple)
padded_payload = LE32(|P|) || P || random(band - |P| - 4)

The padding sits after the real plaintext inside the XOR-and-encrypt zone, so it is indistinguishable from random and is trimmed losslessly on decryption via the length prefix. With bucketing enabled, the ciphertext size reveals only which band the plaintext falls into, not its exact length. A residual coarse leak remains by design (a message in the 257-1024 band could be anywhere in that range); this is the standard length-privacy trade-off and is documented honestly rather than claimed away.

Bucketing is opt-in at the primitive layer so that the four reference SDKs remain byte-for-byte wire-compatible by default. The user-facing surfaces that produce standalone artefacts an adversary might measure - the protect seed-phrase wizard and the structured-record helper - enable bucketing by default.

3.3 Decryption

1. Parse: S = ciphertext[0:32], IV = ciphertext[32:48], E = ciphertext[48:]
2. Derive key: K = Argon2id(SHA-256(pw1) || SHA-256(pw2), S, t=3, m=64 MiB, p=1, v=0x13)
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 Argon2id 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 (cryptographic core). Given a ciphertext E and two control files C and C', an adversary - even one holding the passwords - cannot determine from the cryptographic construction alone 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 XOR operation is symmetric and reversible; both control files are equally valid derivations from the intermediate value X, so the construction itself carries no marker of temporal order.

Distinguishers outside the cryptographic core. The theorem above is about the primitive. Honest deniability analysis must also account for everything an adversary can measure about the artefacts and their contents. We enumerate every distinguisher we are aware of, and the status of each:

  1. Control-file size. If the real and decoy control files differ in length, the adversary picks the real one by file size with zero cryptanalysis. Closed: all control files for a given ciphertext are generated at exactly the ciphertext-payload length, so they are byte-for-byte equal in size (regression-tested: realCtrl.length === decoyCtrl.length on both the record and protect paths).

  2. Ciphertext length. The ciphertext byte-count leaks |P| + 4 unless padded. Closed for the artefact-producing surfaces via length bucketing (Section 3.2.1): the size reveals only a coarse band. A residual band-level leak remains by design and is documented as a limitation, not claimed away.

  3. Deep content validity. A real secret passes its type's integrity check - a real card satisfies the Luhn checksum, a real 24-word phrase has a valid BIP-39 checksum, a real IBAN satisfies mod-97, a real Bitcoin WIF key has a valid Base58Check checksum. A decoy that matched only the surface shape but failed the checksum would be a distinguisher: the adversary keeps the candidate that validates. Closed for the structured-decoy engine: generated decoys are required to pass the same deep-validity check as a real value of that type (Section 4.4), so validity is no longer a real-vs-decoy signal.

  4. Plaintext plausibility. Whether a decoy reads as a believable secret in context is a human judgment, not a cryptographic property. This is irreducible: a decoy that says "this is a decoy" provides no practical deniability regardless of how perfect the cryptography is. deny.sh's decoy-realism engine raises the floor here (shape- and checksum-correct values) but cannot make a contextually absurd decoy plausible.

Distinguishers (1)-(3) are mechanical and are closed by construction in the surfaces that emit standalone artefacts. Distinguisher (4) is operational and bounded, not eliminated; it is the user's responsibility to choose a plausible decoy, and it is treated as a first-class limitation in Section 4.6.

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 Decoy Realism: Shape and Deep Validity

Value-deniability is only as strong as the decoys are believable, and believability has two mechanical layers an adversary can test before any human judgment is involved:

TypeDeep-validity check
Credit cardLuhn (mod-10)
BIP-39 seed phrase (12-24 words)BIP-39 checksum (SHA-256 of entropy)
IBANISO 7064 mod-97
Bitcoin WIF keyBase58Check (double-SHA-256)
Solana key64-byte Base58 decode
Ethereum key32-byte hex
UK NHS numbermod-11

Generated decoys are rejected and regenerated until they pass both the shape and the deep-validity check, so a coerced decoy decryption yields a value that is indistinguishable from a real one on shape and checksum grounds. Types whose integrity is purely structural (opaque API tokens, random hex keys, free-form secrets) have no additional checksum to satisfy, so shape-correctness is already the full mechanical bar for them.

This closes distinguisher (3) from Section 4.2. It does not address distinguisher (4): a checksum-valid card number is still implausible as a decoy if the surrounding context makes a card number nonsensical. Deep validity is necessary, not sufficient, for plausibility.

4.5 Honey Mode: Synthetic Wall-Branch Output

4.5.0 The residual distinguisher

The base construction (Section 3) is value-deniable for every well-formed control file: real and decoy plaintexts are algebraically interchangeable (Section 4.2). But a control file derived from a wrong password is not well-formed. Decrypting under the wrong key produces an intermediate value X' that is uniform-pseudorandom, so XOR-ing it against the control data yields a payload whose 4-byte length prefix is, with overwhelming probability, band-inconsistent: it decodes to a length that does not fit the bucket band the record was padded to. We call this the wall branch. The accidental-well-formed-frame probability for a wrong key is (band - 3) / 2^32 (about 1.4e-8 for the 64-byte band), so the wall fires essentially every time.

Classic behaviour surfaces the wall branch as random noise or a decode failure. For an adversary brute-forcing a leaked artefact, that noise is itself a signal: it tells them they have not yet hit a well-formed door, so they keep guessing. The boundary between "noise" and "a real or decoy plaintext" is observable across many attempts, and it confirms that the artefact is a deny.sh record being searched rather than already-recovered. The cryptographic core does not address this, because it is a property of the failure output, not the success outputs the deniability theorem covers.

Honey Mode (opt-in per record, structured types only) closes this distinguisher by replacing the wall-branch noise with a deterministic, type-correct, plausible fake. A wrong password no longer yields detectable garbage; it yields a value that looks exactly like a successful decryption of a secret of the declared type. The attacker who tries a wrong password gets a Stripe key, a seed phrase, or an IBAN that passes its own checksum, with no way to tell it apart from a real or pre-positioned-decoy decryption.

4.5.1 Wall-branch construction

Honey decrypt does not change the envelope. It adds a verdict and a fallback around the existing decrypt. After recovering the inner payload, the host checks band-consistency of the length prefix:

isWellFormedFrame(payload, band):
    if |payload| < 4: return false
    length = LE32(payload[0:4])
    return length <= |payload| - 4  AND  length <= band - 4

decryptHoney(ciphertext, controlData, {p1,p2}, honeyType, band):
    {payload, salt, wellFormed, plaintext} = decryptToPayload(ciphertext, ..., band)
    if wellFormed:  return utf8(plaintext)                       # real or decoy slot, unchanged
    else:           return generateHoneyDecoy(honeyType, payload, salt)   # the wall branch

The well-formed branch is the classic path: it returns whatever real or pre-positioned-decoy plaintext the control file resolves to. Only the wall branch changes. encryptHoney always sets padToBucket, so band is well-defined and the band-consistency check has a known target. The record stores no per-slot authenticator, no MAC, and no slot list (this is "Approach A"): nothing extra goes on the wire, so Honey Mode adds no distinguisher for how many real or decoy doors a record has.

4.5.2 Seed derivation and the independence property

The fake is generated from a 32-byte seed:

HONEY_DOMAIN = "deny-sh/honey/v1"   (UTF-8, 16 bytes)

seed = SHA-256( HONEY_DOMAIN || 0x00 || decryptBytes || 0x00 || salt || 0x00 || typeTag )

where decryptBytes is the wrong-password wall-branch payload, salt is the record's public 32-byte salt, and typeTag is the declared structured type (e.g. stripe-live-key). The three 0x00 bytes are literal domain separators. The seed depends on these three inputs and nothing else. This single design choice carries the security argument for Honey Mode, and it has two consequences:

  1. Stable per wrong password (no retry tell). Because the seed is a pure function of the wrong-password decrypt bytes (and fixed public salt and type), the same wrong password always lands the same fake. A naive honeypot that synthesises a fresh random fake on each attempt is trivially detected: the attacker tries the same wrong password twice, sees two different "secrets", and concludes the output is synthetic. deny.sh's fake is deterministic, so a repeated guess yields a byte-identical result, exactly as a real decryption would.

  2. Independent of the real secret (no leak). decryptBytes on the wall branch is AES-256-CTR-Decrypt(K', IV, E) XOR controlData under the wrong key K'. It is uncorrelated with the real plaintext: changing the real secret (re-encrypting with a different value under the same passwords) does not change what a given wrong password decrypts to, because the wrong key never recovers the real intermediate value. The seed therefore leaks nothing about the real secret, and the synthesised fake cannot be regressed back toward the protected value. Ports MUST NOT mix any other material into the seed, precisely to preserve this property.

4.5.3 Indistinguishability of the honey branch

The honey branch is engineered to be externally inseparable from the well-formed branch:

The one distinguisher Honey Mode does not claim to eliminate is contextual plausibility (distinguisher 4 of Section 4.2): a checksum-valid card number is still implausible where a card number makes no sense. Honey Mode raises the mechanical floor to its maximum; it does not make a contextually absurd fake believable.

4.5.4 Structured-types-only boundary

Honey Mode is supported for 63 of the 69 known types. Six are refused (isHoneyEligible returns false; encryptHoney/decryptHoney throw): the two unstructured catch-alls, generic and freeform-secret, plus four structured types whose record-decoy output is a JSON blob or multi-branch connection string (jwt-token, postgres-uri, mongodb-uri, gcp-service-account-key) and so cannot yet be reproduced byte-identically across the Rust/Python/Go ports. This boundary is honest, not arbitrary: unstructured data has no shape or checksum to imitate, and the four deferred structured types would risk a real-vs-honey divergence across SDKs. Better to refuse Honey Mode for those and leave them on the classic real+decoy+noise model where the user supplies their own plausible decoys, than to emit a fake that would itself be a distinguisher.

4.5.5 Determinism, cross-SDK parity, and versioning

If the same record were honeyed in TypeScript and decrypted in Rust, a divergent fake would itself be a distinguisher (the attacker who tries the same wrong password against two SDKs would see two different "secrets"). Honey output must therefore be byte-identical across all four SDKs. This is achieved by pinning every determinism surface:

All three are frozen by a shared Known-Answer-Test vector set; no port ships until it reproduces every vector. The HONEY_DOMAIN = "deny-sh/honey/v1" string binds the seed to this exact contract. Any change to the seed layout, the DRBG, or a generator's draw order is a breaking change and requires a new domain string (deny-sh/honey/v2), because mixed-version SDKs would otherwise synthesise mutually-inconsistent fakes for the same record. The version is part of the seed input precisely so that incompatibility surfaces as a different fake rather than a silent mismatch.

4.6 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. The decoy engine enforces shape- and checksum-correctness (Section 4.4), which removes the mechanical distinguishers, but it cannot judge contextual plausibility: a decoy that reads "this is a decoy", or a checksum-valid card number where a card number makes no sense, provides no practical deniability. Choosing a contextually believable decoy remains the user's responsibility.

  2. Length privacy is bucketed, not exact. With length bucketing enabled (Section 3.2.1), the ciphertext size reveals only a coarse band rather than the exact plaintext length. This is a deliberate trade-off: a smaller leak in exchange for bounded ciphertext bloat. An adversary still learns which band a message falls into. Callers requiring uniform artefact sizes within a class can pin an explicit bucket size.

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

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

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

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

  7. Argon2id parameter choice. t=3, m=64 MiB, p=1, v=0x13 is the deny.sh interactive default. It deliberately exceeds the current OWASP minimum (t=2, m=19 MiB, p=1) while staying practical for SDK and browser use. The shipping SDKs hard-pin to these defaults for byte-compatibility; a configurable-parameter API and archival presets for high-value targets are on the post-launch roadmap.

  8. Honey Mode is opt-in and structured-types-only. The synthetic wall-branch output described in Section 4.5 closes the residual "wrong password returns obvious noise" distinguisher, but only for the 38 structured secret types whose shape and checksum can be imitated convincingly. Generic and free-form secrets are deliberately refused (Section 4.5.4) and remain on the classic real+decoy+noise model, where the wall branch is still observable to a patient brute-forcer. Honey Mode is also a per-record opt-in, not a default, so any record encrypted without it retains the classic wall behaviour.

5. Threat Model

5.1 Adversary Capabilities

deny.sh is designed to resist the following adversary:

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 Argon2id KDF
Statistical analysis of control files Empirically verified uniformity (Section 4.3)
Control-file size comparison Real and decoy control files are byte-for-byte equal in length (Section 4.2, distinguisher 1)
Ciphertext-length measurement Bucketed to a coarse band on artefact-producing surfaces (Section 3.2.1)
Decoy fails an integrity check (Luhn, BIP-39 checksum, mod-97, Base58Check) Decoys are required to pass the same deep-validity check as a real value (Section 4.4)
Wrong-password decrypt returns obvious noise (a brute-force progress signal) With Honey Mode enabled on a structured-type record, the wall branch returns a deterministic, type-correct, checksum-valid fake instead of noise (Section 4.5)
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
Contextually implausible decoy content Shape and checksum are enforced, but contextual plausibility is human judgment, not a crypto property (Section 4.4)
Exact plaintext length within a bucket band Length is bucketed, not hidden; a coarse band leak remains by design (Section 3.2.1)

6. Operational Deniability: The Infrastructure Layer

Sections 2-5 specify a primitive that is complete for an individual encrypting their own data on their own machine. The audiences deny.sh is built for, set out in Section 1.3, operate under conditions the primitive does not by itself address: an AI agent platform holds thousands of tenants' secrets behind one process; a custody product keeps hot-wallet credentials that must survive both subpoena and breach; a regulated organisation must produce a tamper-evident account of who decrypted what, when, without that account itself becoming a confession.

For these settings the deniability property is necessary but not sufficient. A decoy that resolves perfectly under cryptanalysis is worthless if the surrounding system leaks the real plaintext through an access log, lets one tenant's compromised credential reach another tenant's key, or cannot serve the ciphertext at all when the hosted service is down and the subject needs plausible cover now. This section specifies the operational guarantees that close those gaps. They are not cryptographic claims; they are system-design claims, and we state them with the same precision so they can be audited against the running service.

6.1 Per-Tenant Cryptographic Isolation

The multi-tenant runtime never derives one tenant's key in a context where another tenant's credential is in scope. Tenant identity throughout the server is the SHA-256 hash of the API key (apiKeyHash), never the raw key; the raw key exists only transiently at the request boundary and is never persisted, logged, or used as a map index. Rate limits, usage accounting, audit chains (Section 6.2), and bring-your-own-key configuration (Section 6.3) are all keyed on this hash, so a cross-tenant reference is a hash mismatch rather than a policy decision that can be misconfigured open.

This matters most for the platforms whose own threat model includes a compromised internal component. An AI agent that has been prompt-injected, or a service account that has been phished, is an adversary already inside the tenant boundary. deny.sh's isolation is designed so that such a component, operating with one tenant's credential, cannot reach across to a second tenant's wrapped key material even though both tenants are served by the same process and, in the bring-your-own-key case, the same cloud IAM principal.

6.2 Tamper-Evident Audit Chain

Every privileged operation on a tenant's data (encrypt, decrypt, deny, key rotation, operator drill-down) appends an entry to a per-tenant, append-only, hash-chained evidence log. The chain is constructed so that any retroactive edit, deletion, or reordering is detectable by recomputation:

prev_hash(entry 1)   = SHA-256(tenant_id || "GENESIS")
prev_hash(entry N>1) = entry_hash(entry N-1)          (same tenant)
entry_hash(entry N)  = SHA-256( prev_hash || tenant_id || op_type
                               || canonical(op_payload) || created_at )

canonical(op_payload) is deterministic JSON with recursively sorted keys and no incidental whitespace, so the hash is reproducible across implementations and languages. verifyAuditChain(tenant_id) walks the chain from genesis and returns the first broken link, if any; the operator dashboard runs this verification continuously and surfaces a tamper flag rather than a silent pass. The payload recorded is metadata and event-occurrence only (operation type, timestamp, key-hash prefix) and never plaintext, key material, or decoy bytes, so the audit log itself cannot become the leak it exists to detect.

The hash chain establishes order and integrity but not time: a party who controls the database could in principle recompute a self-consistent chain over backdated entries. deny.sh therefore anchors each entry to an external clock. A background worker sweeps for entries that lack a granted timestamp and requests an RFC 3161 timestamp token over the entry hash from independent Trusted Timestamp Authorities (FreeTSA as primary, DigiCert as fallback), persisting the returned tokens in a separate audit_chain_tsa table. Each request carries a nonce; verification (verifyTimestampToken) checks PKIStatus == granted, that the token's message imprint matches the entry hash, that the returned nonce matches the one sent, and that the TSA signing certificate's chain validates and was temporally valid at the asserted genTime. An open-source verifier re-proves both the hash chain and the TSA tokens offline, and signed PDF receipts are available on demand for a regulator or counterparty: this record was protected with deniability at timestamp T, here is the proof. The chain thus carries third-party-witnessed time without any party (deny.sh included) being able to forge it after the fact.

The deniability interaction is deliberate. The log proves that a decrypt occurred and when, which a regulator or incident responder can require, without proving which plaintext was returned, which is precisely what the subject must be able to withhold. Accountability and deniability are usually presented as opposites; the metadata-only hash chain is how deny.sh provides both at once.

6.3 Bring-Your-Own-Key and the Confused-Deputy Guard

Tenants that require their key material to live in their own cloud account use bring-your-own-key (BYOK): the data encryption key is wrapped by a customer-controlled AWS KMS key, reached by deny.sh assuming an IAM role in the customer's account. A shared multi-tenant service that assumes roles into many customer accounts is the textbook setting for a confused-deputy attack, in which tenant A's stored role configuration is replayed to reach tenant B's resources.

deny.sh closes this with a mandatory per-tenant ExternalId. A 128-bit cryptographically random value (deny-sh-byok-<32 hex>) is minted per tenant at registration, persisted against the tenant hash, and supplied on every sts:AssumeRole call; the tenant pins the matching value in their role trust policy's sts:ExternalId condition. The guard is fail-closed at two layers: both the store-wrapper and the KMS adapter refuse to issue an AssumeRole without the ExternalId, and the value is folded into the cache key for minted STS credentials so a session obtained under one tenant's guard can never be served to another. The identical pattern protects the AWS Secrets Manager integration. A tenant's role can therefore be assumed by deny.sh only when the call carries that tenant's secret external identifier, which an attacker replaying a different tenant's configuration does not possess.

6.4 Federated Identity (SAML)

Operator and team access for enterprise tenants is brokered through SAML 2.0 with just-in-time provisioning, so a customer manages deny.sh access from their own identity provider and deprovisioning is immediate when an employee is offboarded there. The implementation enforces the standard assertion invariants inline: assertions must be signed (wantAssertionsSigned); NotBefore/NotOnOrAfter are checked with a bounded 60-second clock-skew tolerance; the assertion Issuer is cross-checked against the tenant's configured IdP entity ID to defeat IdP-substitution; a 24-hour replay table rejects re-presented assertion IDs; and an email-domain allowlist gates provisioning. First login creates a tenant-scoped account idempotently keyed on (tenant, name_id); repeat logins reuse it. Account creation is thereby bound to a signed assertion from the tenant's own IdP rather than to a shared password.

6.5 Continuity

Deniability is time-critical: cover that is unavailable at the moment of compulsion is no cover at all. deny.sh's continuity story is that the cryptographic primitive never depends on the hosted service. The same byte-exact construction is shipped as an Apache-2.0 SDK in four languages (Section 7.3) and as a local-mode MCP server whose encrypt, decrypt, deny tools run with zero knowledge and require no API key (Section 7.9). A tenant can encrypt, produce a decoy, and decrypt entirely offline using only the open-source library, with the hosted runtime contributing audit, isolation, and key-management value but never gating the deniability itself. Self-hosting under the AGPL-3.0 application layer, or a commercial licence, is the supported path for organisations that require the operate layer to run inside their own perimeter.

6.6 What the Infrastructure Layer Does Not Claim

The guarantees above are operational, not cryptographic, and they inherit the primitive's limitations from Section 5.3 rather than removing them. Per-tenant isolation bounds a compromised credential; it does not defend a host whose memory is being read by an adversary with code execution. The audit chain detects tampering with the log; it does not prevent an operator with live database access from reading metadata in real time, which is why the recorded payload is metadata-only by construction. BYOK moves key custody to the tenant; it does not protect against a tenant who is themselves the adversary. None of these mechanisms address the adaptive-compulsion scenario of Section 5.3, which remains an operational-deniability problem the subject must solve by pre-positioning decoys, not one any server feature can solve for them. The infrastructure layer raises the cost and narrows the surface; it does not change what the primitive can and cannot promise.

7. Implementation

7.1 Browser Implementation

The browser tools use the Web Crypto API (AES-CTR) and a JavaScript Argon2id 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 and vault integrations) is under AGPL-3.0.

7.2 API Implementation

The server-side API uses Node.js built-in crypto module (AES-256-CTR) and the same Argon2id KDF parameters as the SDKs. 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.

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

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

7.5 CLI

An enhanced command-line interface wraps the TypeScript SDK with commands for encryption, decryption, vault management, environment and environment variable protection. The CLI supports pipe input, hidden password prompts, and manages a .deny/ directory convention with automatic gitignore management. Runtime dependencies: node:crypto from the Node standard library, hash-wasm for portable Argon2id, and commander for argument parsing.

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

7.7 Cloud Backup

An encrypted backup system creates Argon2id + AES-256-CTR archives of control data for storage on local disk, Google Drive, Dropbox, or Amazon S3. The container envelope is DENY_BACKUP_V1; inside, the bundle uses version byte 0x03 (Argon2id with t=3, m=64 MiB, p=1, 32-byte salt, 16-byte IV, AES-256-CTR ciphertext) with independent key derivation from the backup passphrase. Version 0x02 bundles (16-byte salt) remain readable for legacy compatibility.

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

7.9 MCP Server for AI Agents

A Model Context Protocol server exposes 10 tools (3 local, 7 API) for AI agent integration. Local tools (encrypt, decrypt, deny) operate with zero knowledge and require no API key. This enables AI agents to handle deniable encryption workflows programmatically.

7.10 Agent Framework Adapters

Official adapters wrap the core SDK as a vault-entry-as-tool for LangChain (deny-sh-langchain, Python + TypeScript), the Vercel AI SDK (deny-sh-vercel-ai, TypeScript), and the OpenAI Agents SDK (deny-sh-openai-agents, Python + TypeScript), over a shared framework-agnostic core (deny-sh-integrations-core). The contract is identical across adapters: the credential resolves and is consumed inside the tool boundary via a use(secret, args) callback; only a narrowed DTO returns to the model, and a fail-closed leak sweep throws (DenyLeakError) if the raw secret appears anywhere in that DTO. The adapters add nothing to the security model beyond the core; the deniability boundary remains cryptographic, not policy. The OpenAI Agents adapter additionally disables the SDK's default error self-heal (errorFunction: null in TypeScript; a direct FunctionTool construction in Python) so secret-scrubbed errors propagate fail-closed rather than being summarised back into the model context.

7.11 Dependencies

The TypeScript SDK is Apache-2.0 and uses Node's crypto module plus hash-wasm for Argon2id. Browser tools use Web Crypto for AES-CTR and a bundled WebAssembly Argon2id implementation for key derivation. No network calls are made during local encrypt or decrypt operations.

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

9. Verification

All claims in this paper can be independently verified:

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

10. Future Work

References

  1. Canetti, R., Dwork, C., Naor, M., Ostrovsky, R. (1997). "Deniable Encryption." CRYPTO '97.
  2. Biryukov, A., Dinu, D., Khovratovich, D. (2021). "Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications." RFC 9106.
  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