Fieldseal
A portable specification and reference implementations for transparent field-level encryption-at-rest at the data-access layer.
Latest release: v0.1.3 (experimental)
Not independently reviewed, not for production data. The format may change before 1.0, and data written now may have to be re-encrypted if review changes a construction. Writing refuses until provisional use is explicitly armed.
The two reference cores and two ORM adapters can now be installed, for evaluation and feedback:
- Python:
pip install "fieldseal[argon2]"· Django:pip install fieldseal-django - TypeScript/Node:
npm install @fieldseal/core· Prisma:npm install @fieldseal/prisma
If you installed fieldseal-django 0.1.2 or earlier, upgrade. Those
releases did not declare the Argon2id dependency, so a PyPI install could not
save to an indexed column. 0.1.3 fixes the dependency; the format, the cipher
suite and the test vectors are unchanged. The argon2 extra on the Python core
is what Argon2id blind indexes need; leave it out only if you use none.
Both cores pass the same 146 test vectors. Each decrypts what the other encrypts: CI checks this on every run, and the release process checked it again on the published packages. That shows the implementations agree with each other and with the specification. It does not show that the design is sound. That needs independent cryptographic review, which is the gate on 1.0.
Release notes · How to review the design
The problem
A company holding regulated consumer data has three options today, and all three are bad.
Storage-layer encryption (TDE, encrypted volumes) defends exactly one thing: physical loss of a disk. It gives transparent decryption to anything that can authenticate to the database. PCI DSS v4.0.1 Req. 3.5.1.2 says so explicitly, and has been enforceable since 31 March 2025.
Building application-layer encryption yourself took 37signals roughly two
years of a senior engineer’s time, for one framework in one language – with an
abandoned first prototype, an RCE via Marshal serialization caught by luck, and
a deterministic-encryption flaw found by audit days before launch.
Buying a data-privacy vault starts around $12k–$23k/year plus per-tenant fees, and requires either moving your PII into a vendor’s vault or routing traffic through a proxy that discards your ORM’s semantics.
Underneath all three sits a problem nobody has addressed: there is no portable format. Data encrypted by Rails cannot be read by a Python job. Every implementation invents its own ciphertext layout, so application-layer encryption becomes a one-way door into a single language ecosystem.
What this is
- A specification – a self-describing ciphertext envelope for a single database cell, a frozen cipher-suite registry, a key hierarchy, a blind-index construction with a declared leakage budget, and a key-provider interface. With machine-readable test vectors.
- Reference implementations – a core library per language (Python, TypeScript, Java, .NET, Go) that all pass the same vectors, plus thin per-ORM adapters. Core knows nothing about SQL; adapters know nothing about cryptography.
- An operational playbook – threat model, data-classification gate, zero-downtime migration, key-rotation runbook, KMS-outage degradation modes, and published benchmarks.
How the pieces fit. Two implementations in different languages share one
database row: what core A seals, core B opens with the same key, and a blind
index written by one is queried by the other. Adapters route values to the core
and hold no cryptography. Both cores unwrap tenant keys from the KMS in
warm(), never on the value path, and both must pass the same pinned test
vectors. Each part has its own diagram: the write path,
the read path, the equality query and
the key hierarchy.
What this is not
- Not protection against a compromised application process. The keys are in that process.
- Not range queries, sorting,
LIKE, or full-text search over ciphertext. Order-preserving and order-revealing encryption are explicitly forbidden by the spec; the attack literature is unambiguous. - Not a replacement for storage-layer encryption. Keep TDE underneath.
- Not a hosted service, proxy, or vault.
- Not a GDPR Article 17 erasure guarantee. No regulator has endorsed key destruction as standalone erasure.
Documents
- Research Memo — Application-Layer Encryption at the Data-Access Layer Status: Draft 1 · Date: 2026-08-08 · Scope: landscape, prior art, regulatory basis, gap analysis Purpose: establish, with citations, whether …
- PRD — Fieldseal Fieldseal: a portable specification and reference implementations for transparent field-level encryption-at-rest at the data-access layer. …
- Fieldseal Core Specification Version: 0.1-draft · Date: 2026-08-08 (rev. 2026-08-22) · Status: Working draft. Not for production use. Not independently reviewed — the …
- Compliance Mapping Date: 2026-08-08 · Status: Draft 1 · Verification: clause numbers below were read from primary sources (eCFR, Federal Register full text, …
- ORM Adapter Notes Date: 2026-08-08 · Status: Draft 1 · Purpose: the per-ORM engineering detail behind the conformance-level matrix in the spec (§10). Written …
- Dissemination and Evidence Track Date: 2026-08-08 · Status: Draft 1 Separation of concerns. Nothing in the research memo, the PRD, the specification, or the code should be …
- Verification Log Date: 2026-08-08 · Method: independent re-verification of the 20 highest-risk factual claims against primary sources (eCFR, Federal …
- Phase 1 Implementation Plan Date: 2026-08-08 · Status: Draft 1 · Purpose: the engineering plan for Phase 1 (“prove the format,” PRD §8): what gets built, in …
- Test Vector Technical Specification Date: 2026-08-08 · Status: Draft 1 · Purpose: the engineering specification for authoring, validating, and consuming the machine-readable …
- Core Library Architecture Specification Date: 2026-08-08 · Status: Draft 1 · Purpose: the language-agnostic architecture every core implementation …
- Python Core Technical Specification Date: 2026-08-08 · Status: Draft 1 · Purpose: the Python binding of docs/09-core-architecture.md. First implementation built in Phase 1; it …
- TypeScript Core Technical Specification Date: 2026-08-08 · Status: Draft 1 · Purpose: the TypeScript/Node binding of docs/09-core-architecture.md. Second Phase 1 implementation; it …
- Django Adapter Technical Specification Date: 2026-08-08 · Status: Draft 1 · Purpose: the engineering design for adapters/django (package fieldseal-django), targeting Django 6.1 / …
- Prisma Adapter Technical Specification Date: 2026-08-08 · Status: Draft 1 · Purpose: the engineering design for adapters/prisma (package @fieldseal/prisma), targeting Prisma 7.x. …
- Conformance & CI Technical Specification Date: 2026-08-08 · Status: Draft 1 · Purpose: how conformance is claimed, proven, and continuously enforced. This is the machinery behind …
- Operational Tooling Technical Specification Date: 2026-08-08 · Status: Draft 1 · Purpose: engineering design for tools/backfill and tools/leakage-estimator. The backfill tool is PRD …
- Cryptographic Reviewer Brief Date: 2026-08-08 · Revised: 2026-08-22 · Status: Draft 2 — the document sent to prospective reviewers Why this exists. Fieldseal is a draft …
- M2 Implementer Brief — building a second core in isolation Date: 2026-08-22 · Status: Draft 1 · Purpose: the handoff given to whoever builds the second reference core. It exists so that the …
- M2 Report — the TypeScript core, built in isolation Date: 2026-08-22 · Status: Draft 1 · Purpose: the deliverable docs/17-m2-implementer-brief.md §5 asks for: the isolation statement, the …
- What Encrypted Search Costs — a plain-language guide Date: 2026-08-31 · Status: Draft 1 · Purpose: what it costs to search an encrypted field, written for the people who decide whether to adopt …
- The Patient-Directory Demonstration — two stacks, one Postgres table Date: 2026-09-09 · Status: Draft 1 · Purpose: design and reasoning for the Phase 1 demonstration application (docs/07 §2, WS-G): a Django …
- The Write Path — one encrypted field, from save() to the database Date: 2026-09-19 · Status: Draft 1 · Purpose: a picture of what the specification requires to happen when an application writes one …
- The Read Path — one encrypted field, from the row to a value Date: 2026-09-19 · Status: Draft 1 · Purpose: a picture of what the specification requires to happen when an application reads one encrypted …
- The Equality Query — finding a row by an encrypted value Date: 2026-09-19 · Status: Draft 1 · Purpose: a picture of what the specification requires to happen when an application looks a row up by …
- Keys — the hierarchy, and the life of a key version Date: 2026-09-19 · Status: Draft 1 · Purpose: two pictures of the specification’s key model: which keys exist and how each is made …
- Phase 1 Retrospective Date: 2026-09-22 · Status: Draft 1, decision made (§10) · Purpose: the third clause of milestone M5’s exit test in …
- Phase 2 Implementation Plan Date: 2026-09-22 · Status: Draft 1; four of the five §5 decisions made by the maintainer on 2026-09-22, the .NET framework open · Purpose: …
- Java Core Technical Specification Date: 2026-09-22 · Status: Draft 1, the tech spec the Java core is built against; stage S1 (scaffold and CI, §8) is built, and no …
- ADR-0001: Profile the AWS structured-encryption format, or define the envelope fresh Status: PROVISIONALLY DECIDED (option C) under Gate 0a, 2026-08-22 — reversible at Gate 0b; spec §13.1 calls this “the …
- ADR-0001 Appendix A — Expressibility mapping: spec §3–§6 onto the AWS structured-encryption format Date: 2026-08-08 · Status: evidence for ADR-0001 (the “open analysis task” named in the ADR’s option A), re-verified …
- ADR-0002: Which FIPS-approvable AEAD for suite 0x0001 Status: OPEN — provisionally deferred to the status quo (option A) under Gate 0a, 2026-08-22, so that envelope arithmetic exists to …
- ADR-NNNN: Title Status: OPEN | ACCEPTED | SUPERSEDED (by NNNN) | REJECTED · Date opened: · Date closed: · Spec refs: · Spec issue: (required before any …
- G1 — §4.6/§3.1: The key-commitment construction is undefined Labels: §4.6 · §3.1 · spec-gap · blocks-vectors · needs-crypto-review Blocks: every commitment/ and envelope/ expected value; the …
- G2 — §7.3: The Argon2id index-derivation invocation is incompletely specified Labels: §7.3 · spec-gap · blocks-vectors · needs-crypto-review Blocks: blind-index/argon2id.json — every Argon2id vector. Gap §7.3 requires …
- G3 — §7.2/§7.4: `truncate(raw, b bits)` bit-level semantics are undefined Labels: §7.2 · §7.4 · spec-gap · blocks-vectors Blocks: both blind-index/ vector files (every truncated expected value). Status: RESOLVED in …
- G4 — §6.2: `tenant_id = null` encoding in `canonical_context` is unspecified Labels: §6.2 · spec-gap · blocks-vectors · needs-crypto-review Blocks: the context/ vector family; every envelope vector for a deployment …
- G5 — §9/§3.4/§6.3: Decrypt error classification order is undefined Labels: §9 · §3.4 · §6.3 · spec-gap · blocks-vectors · needs-crypto-review (partly) Blocks: most of errors/crypto.json; the precedence cases …
- G6 — §9/§10.3: No error code for mode violations; readonly's index-computation stance unstated Labels: §9 · §10.3 · spec-gap · blocks-vectors Blocks: one case in errors/policy.json. Status: RESOLVED in spec 2026-08-09, adopted as …
- G7 — §4.2: Suite 0xFF02's XChaCha20-Poly1305 has no named normative definition Labels: §4.2 · spec-gap · needs-crypto-review (partly) Blocks: confidence in envelope/ff02.json (mechanics are unambiguous in practice; …
- G8 — §7: The blind index's *stored* representation is undefined Labels: §7 · spec-gap · blocks-vectors Blocks: storage assertions in blind-index/ vectors; adapter DDL in docs/12/13 (both carry an interim …
- G9 — §11.1: Sync-only `blind_index` blocks async runtimes for 10–100 ms per Argon2id term Labels: §11.1 · spec-gap Blocks: nothing in the vector suite; L4 adapter ergonomics (Prisma) and Node service latency. Status: RESOLVED in …
- G10 — §3: No plaintext length bound is defined Labels: §3 · spec-gap Blocks: one boundary behavior (rejection consistency), no stored bytes. Status: RESOLVED in spec 2026-08-09, adopted …
- G11 — §6.1/§7.2: The `purpose` / `index-id` grammar is unconstrained Labels: §6.1 · §7.2 · spec-gap Blocks: context/ negative vectors. Status: RESOLVED in spec 2026-08-09, adopted as proposed — docs/02 §6.1 …
- G12 — §7.10/§7.4: The unique-constraint row contradicts the mandated collision band Labels: §7.10 · §7.4 · spec-gap Blocks: adapter DDL guidance (docs/12 checks E002/E005, docs/13 schema shape); no vector expected values. …
- G13 — §10.2/§7.10: Prisma's `in:` — unconditional MUST-reject vs supported membership rewrite Labels: §10.2 · §7.10 · spec-gap Blocks: the Prisma adapter’s conformance claim wording (docs/13 §4); no vectors. Status: RESOLVED in …
- G14 — §6.1/§6.2: `tenant_id` and `row_id` are unbounded, so KDF `info` is unbounded — and platform HKDFs cap it Labels: §6.1 · §6.2 · §5.3 · §7.2 · spec-gap Blocks: one boundary behavior (acceptance/rejection consistency at large contexts); no stored …
- G15 — §9/§3.4, §11.1/§10.3, §4.8, docs/09 §7: four behaviors both cores had to pin without spec text Labels: §9 · §3.4 · §10.3 · §11.1 · §4.8 · docs/09 §7 · spec-gap Blocks: the fmt_ver and rotate cases of errors/; the nfc-casefold-v1 …
- G16 — docs/09 §7, §7.1; §7.6, §10.2: the index boundary can lose information before the core sees it, and the Unicode pin has no currency policy Labels: docs/09 §7 · docs/09 §7.1 · §7.5 · §7.6 · §10.2 · docs/12 · docs/13 · spec-gap Blocks: the adapter obligation docs/09 §7.1 already …
- G17 — docs/09 §8.1, §3.1/§3.2: `KeyProvider` return values have no ownership contract, and what makes the current cores safe is an undocumented defensive copy Labels: docs/09 §8.1 · docs/09 §3.1 · docs/09 §3.2 · docs/09 §8.3 · docs/10 · docs/11 · spec-gap Blocks: an invariant both cores depend on …
- G18 — docs/09 §2, §8: a client cannot report its own validated index registry, so a check `docs/12` §5 specifies as an error cannot be written Labels: docs/09 §2 · docs/09 §7 · docs/09 §8 · docs/10 · docs/11 · docs/12 §5 · docs/13 · spec-gap Blocks: docs/12 §5’s E006, which …
- G19 — §7.5: re-verification says "compare the actual values" without saying under what equality, and on a normalized column the two readings return different rows Labels: §7.5 · §7.2 · §7.1 · docs/09 §7, §7.2 · docs/12 §3.2, §3.3 · docs/13 · spec-gap Blocks: Django L2 and Prisma L2 — the adapter cannot …
- G20 — §10.2 names the ordering throw for one ORM only; ORDER BY, GROUP BY and DISTINCT over a ciphertext column return confidently wrong answers everywhere else Labels: §10.2 · §7.10 · docs/12 §3, §5, §6 · docs/04 §1 · docs/13 §5 · spec-gap Blocks: No stored byte, no derived value, no vector in any …
- G21 — `docs/13` §4 grants `notIn:` an index rewrite that §10.2's scoping and §7.10's table never granted Labels: §10.2 · §7.10 · §7.5 · docs/13 §4 · docs/12 §3.3 · spec-gap Blocks: No stored byte, no derived value, no vector in any family. …
- G22 — `docs/09` §7.1 requires cores to export the assigned-code-point check for adapters; neither core does, and the adapter that needs it has to parse an error message Labels: docs/09 §7.1 · docs/10 · docs/11 · docs/12 §10.2 · docs/13 §9 · spec-gap Blocks: No stored byte, no derived value, no vector in any …
- G23 — plain `COUNT` over an encrypted column: §10.2's aggregate clause refuses a shape that is exact, and the two adapters now disagree Labels: §10.2 · §7.10 · docs/12 §3/§6 · docs/13 §4 · spec-gap Blocks: No stored byte, no derived value, no vector in any family. A live …
- G24 — the two bucket-semantics escape hatches disagree about negation, and each adapter's error message asserts its own answer Labels: §10.2 · §7.10 · §7.5 · docs/12 §3.3 · docs/13 §2.2 · spec-gap Blocks: No stored byte, no derived value, no vector in any family. One …
- G25 — nothing pins the logical-type → plaintext rendering: a date rewritten through the other adapter becomes unreadable to the first, a decimal loses precision silently, and a boolean cannot cross at all Labels: §3 · §10.2 · §12 · docs/09 §7 · docs/12 §2 · docs/13 §1 · spec-gap Blocks: No envelope byte, no derived value, no error code. It …
- G26 — docs/14 §4 gives an out-of-band entry whose operand the language cannot represent no passing status, so no Java or .NET core can claim L0, and no Go core either Labels: docs/14 §4 · docs/08 §5 · spec §3.5 · docs/09 §7.1 · conformance Blocks: Phase 2 entry (docs/26-phase-2-plan.md §1 item 2, milestone …