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:false per 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.

ThreatVector → impactMitigationResidual / assumed control
Context serialization driftA 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 compromiseA 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 tokenA 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 attackerAn 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 tamperAnyone 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 publishThe 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 raceTwo 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 residencyAn 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-GCMAn 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 / rollbackThe 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 exposureA 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.