Cryptographic construction
For security review: verification guide. See how one encrypted file supports a real message and a chosen fake, and why separate control files matter. The public construction and implementation detail follow the overview.
Cyber Essentials Certified
Treehouse in Valhalla Ltd, the UK operator of deny.sh, holds a current Cyber Essentials certificate. Issued by the IASME Consortium (the UK government-appointed accreditation body) and recorded on the BlockMark Registry. Valid 15 May 2026 through 15 May 2027.
Cyber Essentials covers five technical controls: boundary firewalls, secure configuration, user access control, malware protection, and security update management. The certificate is annual; renewal is on the calendar for May 2027.
Draft version: 11 May 2026 · companion to whitepaper v1.2
Why this page exists
AES-256-CTR encrypts the data. Argon2id turns passwords into an encryption key. XOR is a reversible way to combine bytes, used here with a separate control file. The novelty is in how the three are composed to produce a ciphertext that can be decrypted to two different plausible plaintexts under the same two passwords, by swapping the control file. 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
Browser and core SDK encryption takes two passwords and two messages (plaintexts), one real and one chosen fake (decoy). It produces one encrypted file (ciphertext) plus two control files. The passwords do not change between real and decoy; the control file does. The real control file, fed back into decryption with those same two passwords, recovers the real plaintext. The decoy control file, fed back into decryption with the same two passwords, recovers the decoy plaintext. Both control files together with the ciphertext 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. Salt and initialisation vector (IV) need not be secret. This construction does not include an authentication tag. - 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 Argon2id with parameters
t=3, m=64 MiB, p=1, v=0x13, dkLen=32. The output is a single 256-bit keyK. Pre-hashing with SHA-256 fixes the input length and prevents the concatenation 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 real 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. With the correct passwords, swapping the control file is what swaps the plaintext: the real control file yields the real plaintext, the decoy control file yields the decoy. A wrong password produces a different K and therefore a different X, which under XOR with any control data produces a different payload. The length prefix is not an authentication check. An incorrect password/control combination returns plausible garbage without an error; Honey Mode instead returns a fake in a supported format.
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 proposed deniability argument
The proposed claim, pending independent review, 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 argument for review:
- 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, Argon2id 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. External context, metadata and validation against a provider may still distinguish the two messages. The algebra alone does not prove operational indistinguishability.
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, is widely studied, and is hardware-accelerated on supported CPUs. 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 store an authentication tag at this layer. The length prefix is not an integrity check; any application-level authentication needs separate design review. AES-GCM would compose differently with the XOR step and complicate the deniability proof sketch above.
Argon2id (t=3, m=64 MiB, p=1, v=0x13)
The key-derivation function is Argon2id, parameterised at t=3, m=64 MiB, p=1, v=0x13. Argon2id is OWASP's recommended password-based KDF as of 2024. The deny.sh setting deliberately exceeds OWASP's minimum recommendation of t=2, m=19 MiB while staying practical for interactive browser and SDK use. Memory-hardness makes brute-force scale with RAM cost, not just compute, dramatically raising the per-guess cost for an attacker. Password strength depends on how it is generated, not word count alone. Use strong, independently generated passwords.
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. Security of the building blocks does not by itself validate this composition. Independent review of the construction and implementations remains pending.
Length prefix and integrity
The 4-byte little-endian length prefix on the payload serves one purpose: it tells the decryption code where the plaintext ends and any padding begins. It is NOT an authentication tag. A wrong password produces an essentially random X, which XORed with the control data produces an essentially random payload; if the length prefix happens to fit within the envelope, the SDK will hand you the resulting bytes without complaint. The construction is intentionally unauthenticated at the primitive layer, because an AEAD tag would defeat the deniability property (any tag that verifies a plaintext also proves a plaintext exists). Caller-side integrity is the calling application's responsibility: deny.sh decryption hands back bytes, the application checks whether they look like its own data. 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. Without one of those caller-level checks, a wrong-password decryption returns garbage, not an error.
Implementation notes
Implementation details and side channels, unintended signals such as timing or memory exposure, can undermine the intended protection. Review the relevant runtime and deployment, not just the algorithm.
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. Whether XOR processing takes constant time depends on its implementation and runtime. Argon2id is a hybrid construction: the first half-pass is data-independent (Argon2i), and the remainder is data-dependent (Argon2d). Memory work dominates wall-clock time in both halves, and no practical password-recovery side-channel via Argon2id timing has been demonstrated against properly-parameterised instances at our t=3, m=64 MiB setting. This is not a guarantee that every runtime or calling application is free of timing signals. The implementation notes at /trust document the per-SDK approach. Inspect the cryptographic dependencies of the SDK and version you deploy.
Memory hygiene
The real plaintext exists in process memory between decryption and use. Applications should minimise plaintext lifetime and may clear buffers where supported (TypedArray .fill(0) in browsers, zeroize crate in Rust, and appropriate buffer handling 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. Do not assume that the command-line tool or agent tool server guarantees memory erasure or process isolation.
Side-channel resistance
Side-channel resistance, including cache timing and branch prediction, depends on the implementation and host. 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. Each SDK locks the construction with three cross-SDK Known-Answer Test vectors (KAT1 covers deriveKey with fixed salt 0xAA, KAT2 covers deriveKey with fixed salt 0x01, KAT3 covers a full encrypt cycle byte-exact). The KAT3 fixture exercises the deny operation: a single ciphertext decrypts to multiple known plaintexts under different control files. Wider language-specific suites surround the KAT vectors: inspect the current TypeScript suite, Rust tests named kat_*), Go tests named Test*KAT), and the Python TestKATCrossCompatibility class. Passing known-answer tests (KATs) checks those specific cases, not every input or a complete security proof. 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 two static modules from deny.sh's static origin: the deny.sh logic itself and the hash-wasm Argon2id bundle. Both are 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 each served bundle, re-derives the SHA-256 hash, and compares it to the latest released SRI hash. A matching hash shows that the downloaded bytes match the published fingerprint. It does not by itself prove that the code is safe or that a build is reproducible. 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
These remain limits or unfinished assurance work:
- 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.
Engineering controls around the primitive
Hosted controls add activity records, key custody and service integrations around the encryption design. Review each control’s scope and plan availability; none replaces an independent cryptographic audit.
- Audit chain with signed receipts. Tamper-evident hash chain over recorded operations, RFC 3161 timestamped by an independent TSA. Receipts are verifiable without trusting deny.sh. Dashboard at /dashboard/audit. Receipts support review of recorded operations, not proof of all possible access or regulatory acceptance.
- Durable usage metering. Per-tier daily metering, response headers, 80 / 95 / 100 percent email alerts. Quota events join the audit chain. Closes the silent-overage failure mode.
- BYOK envelope (AWS KMS). Server-stored ciphertext wrapped with a per-record AES-256-GCM DEK, the DEK itself encrypted under the customer's AWS KMS CMK. Customer-controlled blast-radius cap; revocation blocks future unwrapping, not copies already obtained.
- Pre-built integrations. AWS Secrets Manager custodian (third-party-verifiable backup of critical encrypted material), signed outbound webhooks (Datadog, PagerDuty, Slack with exponential-backoff retry and HMAC-SHA256 signatures), SAML SSO with JIT provisioning (Okta-compatible, replay protection, email domain allowlist, cross-tenant collision check).
- Compliance documents. Trust centre, public controls map against SOC 2 TSC and ISO 27001 Annex A, lightweight status page, honest roadmap blog. Closes the procurement-evidence gap.
For the public controls map between these controls and the SOC 2 / ISO 27001 framework criteria, see /compliance#controls-map. For the scan-friendly view, see the Trust centre.
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.
- Biryukov, A., Dinu, D., Khovratovich, D. (2021). Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications. RFC 9106.
- 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.
- /trust: 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. Include your review scope and any deadline. Suggested corrections to this page or the whitepaper are welcomed and credited in the changelog.
Looking for the boundaries?
The threat model has its own maintained page, including host compromise, coercion, external evidence and agent-runtime limits.
Read the threat model ↗What to know before production.
The construction and reference implementations are public for review. An independent cryptographic audit is planned and is not complete. Standard primitives and passing tests do not independently validate their composition.