doppel
Swaps secrets from arbitrary payloads with structurally-equivalent fakes, then restores the originals transparently in the response.
The name comes from doppelgänger: each fake replacing a secret is its structural twin — same format, different value.
See SPEC.md for the behavioral contract.
How it works
secrets.toml
┌─────────────────────────────┐
│ [[structural]] anthropic, … │
│ [[registered]] db-password │
└──────────────┬──────────────┘
patterns
│
┌────▼────┐
payload ───▶ │ swap │── swapped payload ────▶ External
sk-ant-REAL └────┬────┘ sk-ant-FAKE (eg. LLM)
│ │
entries + │
session_key response stream
│ (may contain fakes)
restored ┌────▼────┐ │
payload ◀─── │ restore │◀───────────────────────────┘
sk-ant-REAL └─────────┘ sk-ant-FAKE
swap(payload, patterns) → (swapped_payload, entries, session_key)
restore(response_stream, entries, session_key) → restored_stream
You supply the patterns. swap applies exactly the patterns you pass — nothing
more. Secrets matching those patterns are replaced with structurally-equivalent
fakes before the payload leaves. restore reverses the substitution in the
response stream using the encrypted entries and the session key.
Patterns
You decide what gets swapped. A pattern describes how to detect and replace one secret or one class of secrets. There are two kinds:
Structural patterns
A structural pattern describes the shape of a secret class: an ordered sequence of Literal segments (fixed byte sequences) and Variable segments (a character set with a length range). Detection fires on any payload byte that matches that shape; no prior knowledge of the actual secret value is required.
The library ships built-in structural pattern definitions for common providers (Anthropic, OpenAI, AWS, GitHub, GCP). These are available as a starting set — you opt into them; they are not applied automatically.
Registered secrets
A registered pattern covers a secret that does not conform to any known structural class: you know the actual value and want it swapped wherever it appears. You register the full secret bytes; the library derives a detection fingerprint and generates a fake deterministically from a salt. The original value is never stored.
// Simple registration (default options)
let pat = register?;
// With options: preserve a non-secret prefix, restrict fake charset
let pat = register_with_options?;
SecretOptions lets you declare a non-secret prefix/suffix (preserved
verbatim in the fake) and restrict the fake's character set to match the
original's. register is shorthand for register_with_options with all defaults.
Source: doppel/src/secrets.rs.
Salt — stable fakes across runs
Every pattern carries a salt: a 32-byte random value generated once when the pattern is first registered. The salt is the stability guarantee:
same secret + same pattern + same salt → same fake, every run
Without a fixed salt, each process restart generates a new one and the same secret gets a different fake each time — correct within a single cycle but inconsistent across runs. For the CLI patterns file the salt is written into the file on first use and stays fixed forever; you own it along with the rest of the pattern definition.
Patterns file
The CLI reads patterns from a TOML file (version 2). You create it with init
and extend it with register and define. Each entry embeds its salt, so fakes
are stable across process restarts.
Library users can load a patterns file programmatically:
use ;
let data = read?;
let sf = deserialize?;
let patterns = sf.to_patterns?;
let result = swap?;
Create a new patterns file:
This writes a self-describing TOML file with all built-in structural pattern definitions and freshly generated salts. The registered secrets list starts empty.
Patterns file structure:
= 2
= []
[[]]
= "anthropic"
= "47abb6fb..." # 64 hex chars (32 bytes)
[[]]
= "literal"
= "sk-ant-api03-"
[[]]
= "variable"
= "url_safe_base64"
= 93
= 93
[[]]
= "literal"
= "AA"
# ... more [[structural]] entries for other built-in classes ...
[[]]
= "my-api-key"
= "6d792d..." # hex; detection anchor (first bytes of secret)
= "6c75652d..." # hex; detection anchor (last bytes of secret)
= 36
= "ff3c005b..." # hex; unique per registration
= "8a5843ef..." # hex; HMAC confirmation token
= 3
= 0
# charset omitted → wide default ([A-Za-z0-9!@#$%^&*\-_+.~|])
Valid charset names for structural pattern segments: alphanumeric, url_safe_base64,
uppercase_alphanumeric, digits, hex_lower.
The file MUST be treated with the same sensitivity as the secrets it detects — it
contains detection fragments. On Unix systems, all write operations (init,
register, define) create or update the file with mode 0600.
CLI reference
init — create a patterns file
Creates a new TOML patterns file with all built-in structural pattern definitions
and freshly generated salts. Fails if the file already exists; use --force to
overwrite (warning: regenerates all salts — existing fakes become invalid).
swap — swap a payload
Reads the complete payload from stdin, writes the swapped payload to stdout,
writes the entries (ciphertext; not sensitive on its own) to --entries, and
writes the session key (sensitive; mode 0600) to --key-out.
restore — restore a response stream
Reads the response stream from stdin incrementally and writes restored output to
stdout as each chunk resolves. The session key is supplied only via the
DOPPEL_KEY environment variable — no --key flag exists (command-line
arguments are visible in process listings and shell history).
register — register a secret
|
Reads the secret from stdin (raw bytes, no trimming), appends a new registered-secret
entry to the patterns file, and writes it back atomically. The secret never appears
in command-line arguments. --label is required and must be unique within the file.
Source: doppel/src/secrets.rs (registration logic) · doppel-cli/src/main.rs (run_register).
define — add a user-defined structural pattern
Adds a structural pattern. --segment is repeatable; pass it once per
segment in order. Segment specs:
literal:<value>— fixed byte sequencevariable:<charset>:<min>:<max>— variable-length field from named charset
Valid charset names: alphanumeric, url_safe_base64, uppercase_alphanumeric,
digits, hex_lower.
At least one Variable segment is required. The identifier must be unique in the file.
list — list all patterns
Prints a human-readable summary: structural pattern entries with identifier and segment description; registered-secret entries with label, exact length, and charset summary. Does not modify the file.
inspect — show detail for one pattern
Exactly one of --identifier (structural) or --label (registered) is required.
Prints full detail for the matched entry: all segments and salt fingerprint (first
8 hex chars) for structural patterns; length, charset, and derivation parameters
for registered secrets. Does not modify the file.
remove — remove a pattern
Exactly one of --identifier (structural) or --label (registered) is required.
Removes the specified entry and writes the file back atomically. Removing a
built-in structural pattern identifier emits a warning but succeeds; swap will no longer
detect that secret class.
Streaming
restore processes a stream incrementally. It uses suspicion-driven buffering:
chunks are held only while a potential match is in flight, bounded by the longest
secret length across active patterns (typically 100–200 bytes).
Async streaming (async feature)
[]
= { = "0.0.1", = ["async"] }
With the async feature, RestoreStream wraps entries and session key into a
futures_core::Stream adapter. Pass it any Stream<Item = Result<Bytes, E>> and
it yields restored Bytes chunks as they arrive — no runtime dependency beyond
futures-core and bytes.
For the paranoid
Registered secrets are stored as a detection fingerprint — start_fragment,
end_fragment, exact_length, and hmac_digest (HMAC-SHA256 of the secret
against a per-registration salt) — never as the plaintext value. The source of
truth is doppel/src/secrets.rs.
You can verify any registered entry against its original secret using only
openssl and standard POSIX utilities, and independently reproduce the fake
doppel will generate. See docs/for-the-paranoid.md
for the full audit script and fake-derivation walkthrough.