Repository navigation
Description
Background
Stack Encrypt (packages/stack-encrypt) is the Rust engine that every CipherStash language SDK runs on. It encrypts values, and for searchable columns it writes EQL payloads. EQL is the SQL library we install in the customer's PostgreSQL database. An EQL payload is the JSON value stored in an encrypted column. Its c field contains the ciphertext.
Every ciphertext starts with an envelope header. The header contains the keyset ID, the IV, and the key tag. The key tag is the value ZeroKMS, our key service, needs to give back the data key. Today an EQL scalar payload stores the whole envelope, header included, as Base64 text in c.
The Stack Encrypt API plan, merged in pull request #1365, settled this issue's design on 8 November 2026. This issue first asked for key tag extraction from the envelope. The plan replaces that with a separate header field.
Problem
Two features need the key tag and keyset ID in SQL without decoding the ciphertext:
- Labels. A label is a fact about a value, such as who wrote it, mixed into its key tag. A PL/pgSQL function must read the tag, label it, and write it back in one update. The key policy document describes labels.
- Keyset routing. SQL can see which keyset wrote a row only by decoding the envelope today.
If the tag stays inside c, every SQL function must know the envelope layout. The function must change whenever the layout does. Base64 is also about a quarter larger than Base85 for the same bytes.
The encrypted JSON document (public.eql_v4_json_search) already solves this. It stores a Base64 header h beside a Base85 c (eql-bindings, v4/json_tree.rs).
Proposal
- Add a key header field
hto every scalar EQL payload, in Base64. Use the JSON document header's layout: header version, keyset ID, IV, and key tag. - Store the ciphertext without that header in
c, in Base85. No SQL function decodesc. The JSON document's node ciphertext uses the same type. - Call this payload version 2. Version 1, the form the Go SDK's rc.3 release writes, keeps Base64
cwith the header inside. Version 1 stays readable and searchable. - Move the tag into
h; do not copy it. After labelling, the base tag must not remain anywhere in the row. Otherwise anyone can read the unlabelled tag back. - Leave a bare
Ciphertextunchanged. Its envelope keeps the header inside, and Serde writes it as Base64. AddCiphertext::to_base64, returning the existingBase64Text. Add one field target that stores a bare ciphertext as text, with the working nameBase64Ciphertext. - Keep encoding validity, envelope validity, and authenticated decryption as three separate errors.
Labelling itself, including eql_v4.label_tag, is not part of this issue. The plan tracks it as "Labels: the work". This issue makes the payload ready for it.
Acceptance
- A version 2 scalar payload round-trips in Rust and Go.
- A version 1 payload from the rc.3 test vectors still decrypts and is still found by a query.
- A SQL test reads the keyset ID and the tag from
hwith PostgreSQL's own Base64 decoder, without readingc. - The conformance corpus has version 1 and version 2 payloads.
- Malformed
hvalues are refused with a clear error: wrong lengths, truncation, an unknown version. - Encoding to text seals once, encodes the result, and makes no extra ZeroKMS call. Batching still works.
-
Base64TextandBase85Textcome from one shared place, with no dependency cycle between Stack Encrypt and EQL. The packaging document records that Vitamin C will own them. Which Vitamin C crate is still open.
Relationship to other work
- Follows stack-encrypt: add an owned scalar ciphertext API #1470 and feat(stack-encrypt): add owned scalar ciphertext API #1404. Pull request feat(stack-encrypt): add owned scalar ciphertext API #1404 added
Ciphertext::key_tag. - Coordinate mixed records with stack-encrypt refactor: migrate record derives and plans to owned declarations #1483.
- The compatibility policy in Docs: EQL v4 and Stack Encrypt compatibility policy #1314 must list payload version 2.
Plans: the Stack Encrypt API plan and the conformance roadmap.
Activity
coderdan commentedon Nov 8, 2026
#1567 (labels) depends on this issue. Labelling reads and writes the tag in the key header
h.