The decoy realism engine.
Deniable encryption works on a simple property. One ciphertext, one pair of passwords, but two control files, and the control file you feed back in is what decides which of two plaintexts you get out. Both plaintexts look like the kind of thing somebody would actually encrypt. An attacker with the ciphertext, with the source code, and with the willingness to apply pressure, cannot tell which plaintext is the one you cared about. They get a choice. The choice has to be a real choice.
The cryptographic part of that property is solved. AES, an XOR composition, two passwords, and a control file that decides which plaintext emerges. We have written about the construction at length on the Verify page and in the whitepaper. The construction is the easy part.
The hard part is the decoy. Specifically: where does it come from, and is it any good.
Why this is not a copy-paste problem
For the past eighteen months our decoys came from a hand-curated template pool. A few hundred entries across a handful of categories. Pick a category, sample a template, fill in placeholders, ship. It worked. It also had three problems we could not engineer around.
First, a static pool is a fingerprint. Anyone with the source code, the open weights of our SDK, or a single afternoon with our CLI can enumerate every template we ship. Once enumerated, every future decoy drawn from that pool can be flagged as a known-decoy string. The pool stops being a deniable plaintext and starts being a list of suspect plaintexts. A coercer who has done their homework no longer needs to guess. They check.
Second, the templates were generic. A burner wallet phrase. A made-up API key. A placeholder credit card number. None of them looked like your kind of burner wallet phrase, or your kind of API key. The shape was right. The texture was wrong. A forensic examiner reading the decoy would not immediately catch it, but they would notice that the decoy did not look like a thing somebody actually used.
Third, the pool was bounded by what we had time to write. A few dozen templates per category, capped at whatever the on-call engineer found realistic over a long afternoon. The cap meant repeats. The cap meant gaps. The cap meant we could not deny what we had not pre-written.
The infrastructure version of this product cannot have those three problems. So we built the realism engine.
What we shipped
The decoy realism engine generates a fresh, type-aware, shape-locked decoy per request. It runs in the hosted runtime (the Operate pillar), gated by your API key, rate-limited per tier, cost-capped at the platform level. It covers sixty-nine secret types out of the box: sixty-seven named credential shapes plus generic and freeform-secret as catch-alls for anything outside the named catalogue. Twenty-five of the named shapes have hard deterministic validators (Luhn for credit cards, mod-97 for IBANs, BIP39 checksum for wallet phrases, JWT structural, PEM tag, NHS Modulus 11 for UK NHS numbers, US SSN and UK NI structural rules, Bitcoin Base58Check, and E.164 phone format); the rest are shape-locked by template constraints (prefix, length, character set) without an external checksum. Generation is LLM-driven across multiple provider families. Validation is local, deterministic, and runs before the decoy ever leaves the server.
A representative slice of the catalogue (the full list lives at /tools):
stripe-test-key)sk_test_ prefix, 24+ alphanumeric body. Plausible body, invalid against the Stripe API.stripe-live-key)sk_live_ prefix, 24+ alphanumeric body. Real-shape, inert at Stripe.github-pat-classic)ghp_ prefix, exactly 36 alphanumeric. Length-strict by construction.github-pat-fine)github_pat_ prefix, 60+ chars from [A-Za-z0-9_]. Segment structure preserved.openai-key)sk- (optionally sk-proj-) + 40+ chars. Plausible byte distribution; will not authenticate against any OpenAI endpoint.anthropic-key)sk-ant- (optionally sk-ant-api03-) + 80+ chars. Real prefix shape, plausible character set. Never a key any Anthropic tenant could mint.resend-key)re_ prefix, 20+ chars from [A-Za-z0-9_]. Inert against the Resend API.aws-access-key)AKIA or ASIA prefix + 16 uppercase alphanumeric, total 20 chars. Will not pair with any real AWS IAM identity.bip39-phrase)jwt-token)iban)credit-card)private-key-pem)-----BEGIN ... PRIVATE KEY----- header. Body is base64-shaped. Will not parse as a working keypair.postgres-uri)postgres:// or postgresql:// connection string with credential, host, port, database, query parameters. Inert host.mongodb-uri)mongodb:// and mongodb+srv:// shapes. Plausible cluster naming, inert hosts.gcp-api-key)AIza prefix + 35 chars from [0-9A-Za-z_-]. Google-shaped, inert against Google APIs.gcp-service-account-key)type, private_key_id, client_email, and embedded PEM-shaped private key. Shape-correct, never a working identity.azure-client-secret)[A-Za-z0-9_.~-] with a ~ delimiter. Entra-shaped, not valid for any app registration.azure-storage-key)==. Looks like an account access key, does not authenticate.generic)freeform-secret)The remaining twenty-one named shapes (Slack tokens, Discord bot tokens, Cloudflare API tokens, HuggingFace tokens, Solana and Ethereum private keys, Bitcoin WIFs, Twilio/SendGrid/Mailgun/Resend keys, GCP and Azure cloud credentials, US SSN, UK NI and NHS numbers, E.164 phone numbers, and more) follow the same generate-then-validate pattern. The full enumerated list is at /tools.
For every entry with a hard validator (Luhn, mod-97, BIP39 checksum, NHS Modulus 11, SSN, NI, Bitcoin WIF structural), the post-generation check runs without exception. The decoy comes out of the model, gets parsed against the known structural rules for its type, gets checksummed where a checksum exists, and gets rejected if any check fails that a real value of that type would pass. A BIP39 phrase that fails its checksum is dropped and the model is asked again. A credit-card decoy that fails Luhn is dropped and the model is asked again. The validator is not optional and is not on the model's honour. It runs locally, deterministically, in the hosted runtime, before the response is ever serialised back to your client.
That property is the whole point. We are not asking the LLM to be careful. We are asking the LLM to be plausible, and asking the validator to make sure plausibility does not collide with reality.
What this looks like to a coercer
The threat model is unchanged. A coercer with the ciphertext, your two passwords, and the decoy control file decrypts a plaintext. The plaintext is, by construction, what we just described. It has the shape of a real credential of the type they expected to find. It is the right length. It passes every offline check (Luhn, mod-97, BIP39 checksum) but resolves to no live account at the system of record, which the coercer cannot query in the moment.
What changes with the realism engine is the next move. If the coercer thinks they have caught you out, their next step is to try and use the credential. They cannot. It does not authenticate. Which means one of two things, from their point of view: either they have caught you with a real-but-expired credential, or they have caught you with a decoy. The construction guarantees they cannot tell which.
That is the deniability. Algorithmic in the cryptography, operational in the decoy, structurally enforced at the boundary.
Why a generation engine, and not a bigger template pool
We could have hired a team and grown the template pool to a hundred thousand entries. Three reasons not to.
One. A hundred thousand entries is still a finite set. Anyone with read access to the pool, including any future malicious insider, gets the enumerable list of suspect plaintexts. A generation engine produces something new every time, and the output is not stored on our side at all.
Two. A template pool does not know about your context. The generation engine takes a short type hint and a free-form snippet, and produces a decoy that matches the texture of what you are encrypting, not just the shape. Your decoys for the credentials your platform actually uses will look like the kinds of credentials your platform actually uses.
Three. A generation engine improves. Every quarter the models get sharper, the validators get stricter, and the catalogue grows by adding generators rather than by adding rows. A static pool gets harder to maintain at scale. A generator gets easier.
The first reason is the security-critical one. The other two are the long-run reasons we will not regret this choice.
What we did not change
The cryptographic primitive is untouched. The library still ships Apache 2.0, in four languages, with reproducible test vectors. The SDKs do not call out to our hosted runtime by default. If you want to run deny.sh entirely offline with hand-written decoys, you still can. The realism engine is an opt-in capability of the hosted runtime, billed under your API key, available across all tiers with per-tier daily limits.
Your private data does not leave your client unless you ask for a decoy. When you do, the only thing the engine sees is your secret type and an optional short context hint. We do not see the secret. We do not see your password. We do not store the decoy we sent you. The audit log records the type, the timestamp, and the request id. That is the whole record.
Where this fits in the infrastructure
The decoy realism engine is the first piece of the AI-native moat we have been building toward. It is not a feature on the side. It is the part of the product that compounds. The engine improves with every release: more types, tighter validation, narrower acceptance bounds, smarter type inference. A competitor with a weekend and an LLM can ship a decoy generator. They cannot ship one that has been running in production against real traffic, with a real audit log, a real validator pipeline, and a real disclosure posture behind it. That is the kind of lead a generation engine accumulates by being live and by being part of an infrastructure stack rather than a standalone tool.
The open beta opens on Saturday 4 July 2026 at 08:00 BST. The realism engine is one of the things the open beta will be standing on. It is in the hosted runtime today, behind the same authentication, rate limits, and cost meters as the rest of the API. If you would like early access during the pre-beta window, tell us what you are building.
← More posts on the blog · deny.sh · See the Operate pillar · Why deniability needs infrastructure