Documentation
Security model
What each flare-encrypt layer defends and what it does not: the encryption context as placement integrity, token-scoped tenant isolation, the read-checked freshness anchor, per-environment KEK blast radius, honest crypto-shred, hash-chained audit, and the assumed-control boundary.
What it provides, and what it declines
flare-encrypt provides four properties and honestly declines four. Every guarantee is stated with its residual risk, so nobody markets past the crypto. It provides:
- Confidentiality at rest — a D1 read alone yields only ciphertext and wrapped DEKs; the master key lives outside D1.
- Placement integrity — a ciphertext or wrapped-DEK BLOB relocated to another tenant, column, or version fails the AES-GCM tag.
- Freshness / anti-rollback — a per-
(tenant, column)anchor{ version, ciphertext_hash }in a Durable Object outside D1 is verified on every read, so a D1-write rollback fails on read, not merely on an after-the-fact alarm. - Bounded blast radius and crypto-shred — per-tenant DEKs cap per-key exposure and enable O(1) key destruction, after its erasure windows close.
- Sign-without-export — a signing key (Ed25519 or ES256) born inside the store is rehydrated
extractable:falseper sign call, so no code path can return the private key; every signature is capability-scoped and audited with a hash of what was signed.
It does not provide the following, and these are assumed controls documented loudly rather than pretended away:
- Access control — the encryption context is placement integrity, not authorization. Cross-tenant read isolation rests entirely on token scoping.
- Protection against full-account compromise — an adversary who holds both the KEK store and D1 (or a Worker that can call
env.KEK.get()and read D1) reads everything. - Host-level plaintext hygiene — the CLI writes no plaintext file, but OS swap, core dumps, and the process environment remain the host's responsibility.
- Key commitment — raw AES-256-GCM offers none; each ciphertext is under exactly one DEK, with no multi-recipient message.
The assumed-control boundary
The single most important line: a fully-compromised Worker that can both call decrypt()and read D1 defeats every control below. flare-encrypt reduces blast radius, closes relocation, and anchors freshness in a Durable Object so a D1-write rollback fails on read — but it is not a defense against owning the account it runs in. A breach that rewrites both D1 and the freshness DO still wins.
Trusted (compromise here defeats the design): the master key and the code path that reads it; the isolate executing the SDK; D1 write-integrity and the freshness DO's integrity; the operator's Cloudflare account and credentials. Untrusted (the design defends these): a D1 read in isolation, ciphertext relocation or rollback by a write-capable attacker, over-scoped tokens, the supply chain, and the CLI host.
What each layer defends
The encryption context = placement integrity
The per-tenant-DEK plus context scheme prevents relocation: copy tenant A's blob into tenant B's row and the GCM tag fails. It does nothing to stop an authorized-but-over-scoped token from reading tenant B legitimately, and it is not freshness — two values of the same secret encrypt under the same DEK with a byte-identical context, so their tags are mutually valid. The context is placement integrity, full stop.
Tenant isolation = scoped tokens
A token scoped { project, env, tenant: NULL } reads and exports every tenant's plaintext in that environment through the front door — the store faithfully derives each tenant's context and decrypts. So tenant isolation rests entirely on token scoping: the default is per-tenant tokens, a tenant = NULL wildcard requires an explicit admin orsecrets:export-all capability, and scope resolution compares on immutable IDs and verifies each resolved row is a child of the previous, closing confused-deputy and IDOR paths.
The freshness anchor = anti-rollback on read
A sharded freshness Durable Object holds the authoritative { version, ciphertext_hash }outside D1. Adding version to the context cannot fix rollback — the attacker controls that column too — so the fix lives outside the row entirely, committing to a hash of the currentciphertext. A single-D1-write attacker who rolls a secret back is rejected on read with astale_ciphertext 409. The residual is that the DO is on the read path (an availability dependency that fails closed), and a full-account attacker who writes both D1 and the DO still defeats it.
Per-environment KEK = bounded blast radius
The KEK is per-environment: each environment holds its own key bytes, and a leaked KEK unwraps only its own environment's tenant DEKs. A KEK plus a D1 read compromises that one environment permanently, but no other — the wrap context binds each DEK to its environment via the globally-unique tenant id. Proactive rotation does not contain such a breach; compromise-driven rotation of the affected environment does.
Crypto-shred = honest erasure
Dropping a tenant's wrapped DEK makes its ciphertext unreadable through the live database in O(1), but "permanently unreadable, O(1), done" is false as an immediacy claim. Erasure only completes atmax(D1 Time-Travel window, retained-KEK-version lifetime, DEK-cache TTL) — Time Travel restores up to 30 days, a recovered DEK stays unwrappable while its KEK version is retained, and a cached handle survives up to the cache TTL. The store reports that true completion time.
Audit = hash-chained and mirrored
The audit log is hash-chained so any deletion or edit breaks the chain or leaves a sequence gap, and it is streamed to an immutable Logpush → R2 sink with Object Lock. The chain detects tampering but does not prevent it; the immutable mirror is the only evidence that survives full-account compromise, which is what lets a security engagement answer "your account, not ours" with proof rather than assertion.
Threat register
Each guarantee is paired with its residual risk or the control that is assumed to hold.
| Threat | Vector → impact | Mitigation | Residual / assumed control |
|---|---|---|---|
| Context serialization drift | A change to field order or encoding silently breaks every decrypt, with no error until read time. | A frozen FEAC binary TLV with an fmt version byte and golden hex vectors gating CI in every repo that encodes a context. | Accidental drift is caught; a malicious re-publish that changes encoder and vectors together is a supply-chain problem. |
| KEK compromise | A leaked KEK plus a D1 read yields that environment's tenant DEKs, permanently. | KEK held outside D1, per-environment material, per-access auditable via Secrets Store; two-level version map for O(1) rotate and independent per-env purge. | A dump of a KEK and D1 together compromises only that environment; recovery is compromise-driven rotation of that environment. |
| Over-scoped token | A cross-tenant token reads and exports every tenant's plaintext through the front door. | Token scoping is the only tenant-isolation control: per-tenant by default, wildcard gated behind an explicit capability, scope resolved on immutable IDs. | Least-privilege minting is an operator responsibility; scope resolution must verify each row is a child of the previous. |
| Rollback / replay by a DB-write attacker | An attacker rewrites a prior { ciphertext, iv } back into a row and the tag still validates → silent rollback. | The freshness Durable Object holds the current ciphertext hash outside D1; a read verifies it and returns stale_ciphertext (409) on mismatch. | The DO is a read-path availability dependency; an account that writes both D1 and the DO still defeats it; the R2 audit mirror is the backstop. |
| Crypto-shred incompleteness | "Drop the DEK ⇒ permanently unreadable, O(1)" is false as an immediacy claim. | Report completion as max(Time-Travel, retained-KEK lifetime, cache TTL); pair shred with a KEK-purge step; drop the DEK on every deletion path. | Erasure only completes after all three windows close; never surface "O(1) done." |
| Audit-log tamper | Anyone with account access can edit audit rows in the same D1. | Hash-chain each row with a monotonic sequence; mirror every row to an immutable Logpush → R2 sink; audit-before-act. | The chain detects but does not prevent tampering; the immutable sink is the only evidence surviving full-account compromise. |
| Supply-chain — malicious SDK / CLI publish | The SDK touches every ciphertext and holds unwrapped DEK handles; the CLI handles admin tokens and full plaintext on export. | Exact-pin the packages, lockfile integrity, npm provenance / Sigstore verified in CI, and vendoring where warranted. | The golden test is not a supply-chain control; trust rests on pin plus provenance plus review. |
| First-write DEK race | Two isolates race a new tenant's first write and collide on PK(tenant_id, dek_ver=1) → a secret under a never-persisted DEK. | Atomic get-or-create: insert-or-noop, re-select the winning DEK, unwrap that one, encrypt under it; wrapped DEK committed before any ciphertext. | None if the ordering holds; enforced by a mandatory N-parallel-first-write test. |
| DEK cache residency | An unwrapped handle sits in isolate memory; an attacker who can call decrypt() with it reads plaintext. | Cache holds extractable:false handles only, bounded 512 entries / 300 s insertion-TTL, evicted on disconnect, shred, and rotate. | Cross-isolate residency equals the TTL unless a shred generation is checked on every get; a fully-compromised isolate is out of scope. |
| IV reuse under AES-GCM | An IV collision under one key is catastrophic for GCM. | Fresh 96-bit IV per encrypt and per wrap; per-tenant DEKs cap invocations to one tenant's rows; IV length asserted 12; tagLength a constant 128. | The birthday bound is per-tenant — monitor the encrypt count under each DEK and rotate before the NIST ceiling. |
| Signing-key misuse / rollback | The private key never crosses the API, so the vectors are indirect: an over-scoped keys:sign token signs at will (an oracle is the capability); a D1-write attacker rolls the active version back or swaps a stored public key — a verification DoS, since forging priv_wrapped without the KEK fails the KEYWRAP tag. | Sign-only tokens are tenant-scoped and optionally pinned to one key; cross-tenant signing requires admin; every signature audits { keyVer, sha256(message) }, so a rollback is visible in the chain; compromise runbook = retire the version and rotate. | Not an HSM — key bytes exist transiently in isolate memory during generate/sign; break-glass export is key exposure by design (rotate after use); key rows are not freshness-anchored, a deliberate acceptance since the attack yields signatures under a still-tenant-owned retained key. See Signing keys. |
| CLI plaintext exposure | A value in argv, a swap-out, or a core dump lands plaintext on disk. | fe set rejects a value in argv; fe run sets ulimit -c 0 on the child before exec; execve injection, never a tmpfile; creds in the OS keychain. | The guarantee is "the CLI writes no plaintext file"; OS swap and core-dumps are the host's responsibility, and env injection is visible in the child's /proc. |
One line: flare-encrypt makes a D1 read alone worthless, makes relocation fail, anchors freshness so a D1-write rollback fails on read, and makes key destruction O(1) — and it is honest that authorization, full-account compromise, and host hygiene live outside its boundary. The cryptographic detail behind each control is on the Cryptography page; the operational controls are in Deployment & CLI.