You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit cdeff8b
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: docs/target-directed-encryption.md
+13-8Lines changed: 13 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -193,13 +193,18 @@ let row: EncryptedUser = user.encrypt_into(&cipher).await?; // one batch, no c
193
193
its context (`"<context>/<plaintext field>"`); the prefix is the required
194
194
container `context`, named explicitly — never inferred from the Rust type's
195
195
name, which two types can share and a refactor can change. `#[stash(from = ..)]`
196
-
and `#[stash(context = "..")]` on a field are the overrides, and
197
-
`#[stash(nested)]` marks a field whose type is itself a `struct` derive carrying its own
198
-
contexts (it is handed `()`). The context is the AAD of every stored
199
-
ciphertext in the column, so renaming a plaintext *field* is still a data
200
-
migration: pin the old literal with `context = ".."` first.
201
-
202
-
Leaf, record and field-by-field struct are the same trait, and a column of any of them is `Vec<T>`'s structural impl over the same trait — `ages.encrypt_into_with_context(&cipher, ctx)` for a `Vec<u32>` is one batched call, and `users.encrypt_into(&cipher)` for a `Vec<User>` likewise. Recursion does the rest. Earlier sketches of this design had a separate input-side derive for rows — that was a second mechanism the naming was hiding. (The field-by-field form shipped as `row = ..` and was renamed `struct = ..` on 2026-09-04: "row" pushed database vocabulary into a general-purpose library. `plaintext = T` derives every field from the whole value whatever `T` is — the derive sees a name, not a definition — and `from` / `nested` exist only with `struct`.)
196
+
reads a field from a differently named plaintext field, and
197
+
`#[stash(identity = "..")]` pins the label segment, so the field is keyed under
198
+
`<context>/<identity>`. A field's context is always its record's context plus
199
+
its identity: no field escapes it, and a literal context is one plain segment.
200
+
A field whose type is itself a record is an ordinary field: it is sealed under
201
+
`<context>/<field>`, and its own contexts are extended by that pair. A struct
202
+
derive takes one output per plaintext field; to store several terms from one
203
+
field, type that field `Encrypted<Terms>`. The context is the AAD of every
204
+
stored ciphertext in the column, so renaming a plaintext *field* is still a
205
+
data migration: pin the old name first with `identity = ".."`.
206
+
207
+
Leaf, record and field-by-field struct are the same trait, and a column of any of them is `Vec<T>`'s structural impl over the same trait — `ages.encrypt_into_with_context(&cipher, ctx)` for a `Vec<u32>` is one batched call, and `users.encrypt_into(&cipher)` for a `Vec<User>` likewise. Recursion does the rest. Earlier sketches of this design had a separate input-side derive for rows — that was a second mechanism the naming was hiding. (The field-by-field form shipped as `row = ..` and was renamed `struct = ..` on 2026-09-04: "row" pushed database vocabulary into a general-purpose library. `plaintext = T` derives every field from the whole value whatever `T` is — the derive sees a name, not a definition — and `from` / `identity` exist only with `struct`.)
203
208
204
209
### Relationship to `Encrypt`
205
210
@@ -252,7 +257,7 @@ vitaminc already has the generic notion, twice — `Aad<'a>` (aead) and `PrfCont
252
257
253
258
Context is threaded **per value**, as an argument. It is not baked into the cipher.
254
259
255
-
Whether the *caller* owes one is decided by the target type, at compile time. `Ctx` is a parameter of `EncryptFrom` so that each impl can bound it: a leaf is implemented for `NonEmpty<T>` alone, because it has nothing else to authenticate under and `()` is the empty context it must never derive under; a derived record is implemented twice — for `()`, deriving each field under the context it carries itself (a `context = ".."` literal, or the one a `struct = ..` derive infers), and for `NonEmpty<T>`, deriving each field under that context *extended* with the caller's (`("users/age", id)`), or under the caller's as it is for a field with none. No record accepts a context and then discards it. `encrypt_into(&cipher)` passes `()` and therefore compiles only for outputs whose every leaf has a context of its own; everything else takes `encrypt_into_with_context`, and such an output takes that too when the caller has something to add, a record id say, so a field is bound to its record as well as its name. This is vitaminc's `encrypt` / `encrypt_with_aad` split, with the choice made by the type rather than at every call site.
260
+
Whether the *caller* owes one is decided by the target type, at compile time. `Ctx` is a parameter of `EncryptFrom` so that each impl can bound it: a leaf is implemented for `NonEmpty<T>` alone, because it has nothing else to authenticate under and `()` is the empty context it must never derive under; a derived record is implemented twice — for `()`, deriving each field under the context it carries itself (the one a `struct = ..` derive infers, `<context>/<identity>`, or a plain-segment literal), and for `NonEmpty<T>`, deriving each field under that context *extended* with the caller's (`("users/age", id)`), or under the caller's as it is for a field with none. No record accepts a context and then discards it. `encrypt_into(&cipher)` passes `()` and therefore compiles only for outputs whose every leaf has a context of its own; everything else takes `encrypt_into_with_context`, and such an output takes that too when the caller has something to add, a record id say, so a field is bound to its record as well as its name. This is vitaminc's `encrypt` / `encrypt_with_aad` split, with the choice made by the type rather than at every call site.
256
261
257
262
A scoped cipher (`cipher.for_column("users", "age")`) was considered and rejected: it makes encrypting one record — several fields, several identifiers — into several scoped ciphers, which fights batching for no gain. With context as an argument, a record is one shared `&cipher`, many contexts, one flush.
0 commit comments