Raw capture โ raw/cipherstash.md
url: https://github.com/cipherstash/cipherstash title: CipherStash author: CipherStash date_fetched: 2026-07-21 date_published: unknown
CipherStash is a data security platform providing searchable field-level encryption with identity-bound key management. The cipherstash/cipherstash repo is a "front door" meta-repository that serves as the entry point to a multi-repo product ecosystem. It contains only a README and a discovery skill for AI agents โ the actual implementation lives across four product repositories.
The platform has five products arranged in a layered architecture:
Your application (client-side encryption)
โ
โโโโโโโโโโโโโดโโโโโโโโโโโโ
@cipherstash/stack (TS/JS) CipherStash Proxy (Rust)
Protect.go (Go) transparent, in front of Postgres
โ โ
โโโโโโโโโโโโโฌโโโโโโโโโโโโโ
โ
EQL (searchable ciphertext + indexes in PostgreSQL)
โ
ZeroKMS (unique key per value, derived on demand)
@cipherstash/stack, TypeScript)The primary SDK for TypeScript/JavaScript applications. A pnpm workspace monorepo managed with Turborepo. The language breakdown (83.7% TypeScript, 15.9% PLpgSQL) indicates it ships with PostgreSQL-native components for query execution.
Schema definition uses builder-pattern helpers:
encryptedTable("users", {
email: encryptedColumn("email").equality().freeTextSearch(),
salary: encryptedColumn("salary").orderAndRange()
})
Client-side encryption: client.encrypt(plaintext, { column, table }) returns a discriminated union (.failure / .data) โ a functional error-handling pattern rather than throwing. Model-level operations (encryptModel, bulkEncryptModels) handle entire objects or batches.
Identity binding: Uses OidcFederationStrategy to authenticate as the end user, with .withLockContext({ identityClaim }) binding decryption to a specific identity claim. Data encrypted with a lock context can only be decrypted with the same context.
Integrations: Drizzle ORM, Supabase, Prisma, Amazon DynamoDB.
A transparent PostgreSQL wire-protocol proxy written in Rust. Sits between the application and PostgreSQL, intercepting SQL traffic and encrypting/decrypting on the fly with zero SQL changes.
Dual-stream architecture: Each client connection spawns two concurrent handlers:
- frontend.rs: Intercepts client-to-server messages, runs type inference, encrypts, forwards transformed SQL
- backend.rs: Intercepts server-to-client messages, identifies encrypted columns in result rows, batch-decrypts, returns plaintext
Both share a Context tracking active statements, portals, column metadata, and timing.
Extended query protocol: Tracks state across Parse โ Bind โ Describe โ Execute. At Parse time, SQL is intercepted, type inference runs, literals are encrypted, the AST is transformed, and metadata is stored. At Bind time, parameters are encrypted using Parse-phase metadata.
Five Rust crates under packages/:
- cipherstash-proxy/: Main binary (wire protocol, encryption service, schema management, TLS, CLI)
- eql-mapper/: SQL type inference engine (unifier, type definitions, trait bounds, operator/function signatures, transformation rules)
- eql-mapper-macros/: Procedural macros for declaring operator/function type signatures
- showcase/: Example healthcare data model
A PostgreSQL extension that stores encrypted data as jsonb and provides index term extractors. Written primarily in PLpgSQL with code generation from a Rust catalog.
Three index term types, each optimized for a different query pattern:
| Index Type | Mechanism | PostgreSQL Index | Operations |
|---|---|---|---|
unique |
HMAC-SHA256 (eql_v3.hmac_256) |
Hash index on eq_term() |
Equality (=) |
match |
Bloom filter over n-grams | GIN index on match_term() |
LIKE, ILIKE |
ore |
Order-Revealing Encryption blocks | B-tree on ord_term() |
>, <, BETWEEN, ORDER BY |
Performance characteristics (from EQL documentation): - Equality (hash): 0.43โ0.46 ms, near-constant across dataset sizes - ORE range: 4.1 ms at 10K rows โ 8.1 ms at 10M rows - Bloom match: 1 ms at 10K โ 216 ms at 10M (degrades with dataset size) - GROUP BY on encrypted data: ~3ร overhead vs plaintext
Code generation: The eql_v3 scalar encrypted-domain types are generated from a single Rust constant (CATALOG in crates/eql-domains/src/lib.rs). Each DomainFamily row declares domain name, ScalarKind, and which index terms apply. mise run build invokes cargo run -p eql-codegen to regenerate SQL. Generated files are deterministic and byte-identical for an unchanged catalog, committed in place under src/v3/scalars/<T>/.
Rust crate structure: eql-domains (catalog), eql-codegen (SQL generator), eql-bindings (shared types + generated TS/JSON Schema), eql-tests-macros (SQLx test matrix).
A Go encryption SDK that wraps precompiled Rust static libraries (six platform-specific .a files: macOS ARM64/Intel, Linux ARM64/x64, glibc and musl variants). No Rust toolchain needed by consumers.
Two schema definition approaches:
- Struct tags: cs:"unique", cs:"match", cs:"ore", cs:"ste_vec(prefix=t/c)" with Go type inference (stringโtext, intโnumber, etc.)
- Programmatic builder: protect.NewSchema("users").Column("email", protect.CastAsString).Equality().FreeTextSearch().Done()
Bulk operations: BulkEncryptModels makes "a single KMS call for all fields across all models." Query values are encrypted via EncryptQuery(ctx, col, queryType, plaintext) returning index-specific fields (UniqueIndex, MatchIndex, OreIndex) for use in SQL WHERE clauses.
A key management service that inverts the traditional KMS model: keys are derived on demand based on user identity and access policies, never persisted. Benchmarked at up to ~14ร faster than AWS KMS for high-frequency operations. Keyset isolation provides "provable cryptographic separation, not policy enforcement" for multi-tenant deployments.
CipherStash uses non-deterministic AES-256-GCM for the ciphertext (repeated encryptions of the same plaintext produce different ciphertexts), but accompanies each ciphertext with deterministic search term indexes (HMAC-SHA256 for equality, Bloom filter for text search, ORE for range). This gives the best of both worlds: ciphertexts resist comparison attacks, but the deterministic HMAC term enables equality lookups. The HMAC is keyed per (value, encryption-key) pair, so even the deterministic term reveals nothing to an attacker without the key.
The Proxy's eql-mapper crate implements a constraint-based type inference engine operating at parse time without executing SQL. Types are unified using a Robinson-style unification algorithm adapted for SQL semantics. Every AST node gets a type (Native, EQL, Projection, Var, or Associated). EQL types carry trait bounds (Eq, Ord, TokenMatch, JsonLike, Contain) that form a hierarchy (Ord implies Eq). When two Partial EQL types for the same column meet, their bounds merge (union); when Partial meets Full, the result promotes to Full. The system automatically infers the minimum encryption payload needed for each value โ a smart optimization that avoids encrypting unnecessary index terms.
The Proxy's type inference runs three visitors simultaneously during a single AST walk: - ScopeTracker: lexical scopes, table visibility, column resolution, wildcard expansion - Importer: brings schema into scope, creates typed projections - TypeInferencer: actual inference via the unifier, with per-node implementations
This is more efficient than three separate passes and avoids consistency issues between visitor ordering.
EQL operators and functions are declared with generic type parameters and trait bounds using Rust procedural macros. Associated types (e.g., <T as JsonLike>::Accessor) resolve differently depending on whether T is EQL or Native, allowing the same signature to work for both encrypted and unencrypted columns. Unknown functions fall back to assuming all arguments are native โ a safe default since native types satisfy all trait bounds.
The Proxy collects only non-null encrypted values while tracking original positions, sends them to ZeroKMS in a single batch, then reconstructs the result vector with ciphertexts placed back at their original positions. This minimizes API calls while correctly handling nullable columns.
The Proxy's schema state uses ArcSwap, providing lock-free reads with atomic updates. Query processing never blocks on schema reload. Schema reloads happen on startup (exponential backoff, up to 10 attempts), periodically (configurable interval), and on-demand (DDL detection triggers reload on transaction commit).
DDL statements (CREATE TABLE, ALTER TABLE, DROP TABLE) are captured in a SchemaWithEdits overlay that masks the loaded schema so subsequent statements in the same transaction see updated structure. On commit, the proxy triggers a full schema reload.
SQL AST transformation rules implement a TransformationRule trait and compose via tuple implementation (chains of 1 to 16 rules). Each rule has a would_edit method enabling dry-run optimization: the system first checks if any rule would modify the AST, and only rebuilds if necessary โ for passthrough queries (no encrypted columns), this avoids AST reconstruction entirely.
tsortEQL SQL sources use -- REQUIRE: comments for dependency edges. The build collects edges, resolves them with tsort, and concatenates files in dependency order into a single installer. The eql_v3 surface is self-contained โ no eql_v2.<symbol> reference appears anywhere, enforced in CI.
Rather than storing keys and checking access via policy, CipherStash binds encryption keys to identity claims cryptographically. A user's JWT token is tied to their data keys โ only that user can decrypt their data. This means access is enforced cryptographically, not just by policy evaluation.
The choice between ORE (Order-Revealing Encryption) and OPE (Order-Preserving Encryption) is a deliberate security/performance trade-off. OPE produces ciphertexts that sort under PostgreSQL's native byte ordering (no operator class needed), making range scans cheaper. But OPE reveals more about relative value order. ORE is recommended by default, with the docs noting the choice depends on "performance and threat-model requirements."
Each index type reveals something about the plaintext: HMAC reveals when two values are equal (but only to someone without the HMAC key), Bloom filters reveal overlap in n-gram sets, ORE reveals relative ordering. This is not a bug โ it's a deliberate design choice to make encryption usable. The platform documents this honestly rather than pretending to be perfect.
In most database security products, the database defines which columns are encrypted and how. In CipherStash, the encryption client decides which index terms a value carries. The database has no configuration state for which operations are allowed on a column โ the query surface is fixed by the domain variant at creation time. This keeps security policy in the application layer where developers already manage access control.
The dual approach (SDK and Proxy) lets teams choose based on their constraints. The SDK requires code changes but gives fine-grained control over the encryption schema. The Proxy requires zero SQL changes but adds a network hop and is PostgreSQL-specific. Both share the same EQL foundation.
Traditional KMS stores keys and serves them on demand. ZeroKMS derives keys from identity and policy on each request, never persisting them. This eliminates the storage attack vector entirely but means that key derivation must be fast enough for every encryption/decryption operation. The claimed 14ร speed advantage over AWS KMS is the enabler for this architectural choice.
Rather than building a custom encrypted database, CipherStash extends PostgreSQL with searchable encryption via extensions. This means users keep their existing PostgreSQL tooling, backups, and operational knowledge. The trade-off is that PostgreSQL wasn't designed for encrypted query execution, so edge cases (untyped literals falling back to native jsonb operators, operator mismatch on wrong column types) are sharp.
All EQL domain types, operators, functions, and aggregates are generated from a single Rust constant. This ensures consistency โ the catalog is the "source of truth" and all SQL surfaces derive from it deterministically. No hand-editing of generated files is allowed (CI gate). The constraint that generated code must never use STRICT and must be LANGUAGE plpgsql for blockers (to prevent the planner from skipping RAISE) shows deep PostgreSQL expertise.
Cryptographic separation (one keyset per tenant) rather than application-level policy enforcement. This is "provable" isolation โ even if the application has a bug, data from one tenant can't be decrypted with another tenant's keys. The trade-off is operational complexity: keyset management becomes a new operational concern.
Searchable encryption on standard PostgreSQL โ not a custom database engine, but extensions on the database you already run. This is the "boring technology" approach to a hard problem.
The Proxy's type inference engine โ a full constraint-based type system for SQL implemented at parse time with Robinson unification. Not something you'd expect to find in a database proxy. The three-visitor-in-one-pass design is elegant.
Zero-storage KMS โ keys derived on demand and never persisted. This inverts the entire model of how key management works and eliminates a whole class of attacks.
Honest about information leakage โ the documentation explicitly names what each index type reveals, rather than hiding it behind "military-grade encryption" marketing.
Agent-native design โ the cipherstash/cipherstash meta-repo is explicitly designed for AI agents as first-class consumers, with a SKILL.md discovery skill, llms.txt documentation, and a deterministic product โ repo โ docs map. This is forward-thinking in an era where agents are increasingly the first users of developer tools.
ZeroKMS is complementary (backed by AWS KMS at the root) but inverts the model: KMS stores keys, ZeroKMS derives them. The performance claim (14ร faster) matters because it enables per-value key derivation at query time.
These provide client-side encryption but lose searchability. CipherStash preserves querying via EQL index terms, at the cost of information leakage through the indexes.
Always Encrypted supports equality on deterministic encryption but not range, text search, or JSON queries. CipherStash's multi-index approach provides a richer query surface at the cost of larger ciphertext (each value carries HMAC + Bloom + ORE terms).
Custom app-level encryption (encrypt in app code, decrypt in app code) provides the strongest security but kills all database query capability. CipherStash is the middle ground โ encrypted in transit and at rest, searchable via EQL indexes.
Stack requires code changes (define schemas, call encrypt/decrypt) but works with any database. Proxy requires zero code changes but is PostgreSQL-specific. Both share EQL and ZeroKMS. This is a well-considered two-pronged strategy for different adoption paths.
The cipherstash/cipherstash repo itself is intentionally minimal โ a README plus a skills/ directory containing a single discovery skill for AI agents. The implementation is distributed across:
- cipherstash/stack (TypeScript monorepo, 2,068+ commits, 199 releases, latest stash@0.17.1)
- cipherstash/proxy (Rust, Docker image on Docker Hub)
- cipherstash/encrypt-query-language (PLpgSQL + Rust codegen, published on database.dev)
- cipherstash/protectgo (Go + precompiled Rust static libs)