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 327b4db
Browse filesBrowse the repository at this point in the historyBrowse files
|`fuzz:client-key`|`stack-kms`|`client_key_encoded`|`ClientKey::from_encoded_v1` (hex or base64) |
27
27
|`fuzz:sealed-value`|`stack-encrypt`|`sealed_value_decode`|`SealedValue::from_bytes` (frozen v1 leaf); accepted input must re-encode to itself |
28
-
|`fuzz:term-decode`|`stack-encrypt`|`term_decode`| the SEM term decoders (`EqualityTerm`, `MatchTerm`, `OreTerm`, `OpeTerm``from_bytes`) |
28
+
|`fuzz:term-decode`|`stack-encrypt`|`term_decode`| the SEM term decoders (`EqualityTerm`, `MatchTerms`, `OreTerm`, `OpeTerm``from_bytes`) |
29
29
|`fuzz:check-record`|`stack-encrypt`|`check_record`|`dynamic::record::check_record` over an `Arbitrary`-derived plan and tree, against a model of the record rules (structure-aware) |
30
+
|`fuzz:plan-build`|`stack-encrypt`|`plan_build`|`plan::FieldsBuilder::build` over `Arbitrary`-derived untrusted names (context, fields, identities); a built field's label must render and parse back, and passthrough names are never refused (structure-aware) |
30
31
31
32
Each target is a few lines — `libfuzzer-sys` hands a `&str` (or `&[u8]`
32
33
for the byte decoders) to the parser via the `arbitrary` crate:
Copy file name to clipboardExpand all lines: docs/target-directed-encryption.md
+14-8Lines changed: 14 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -193,13 +193,19 @@ 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. No field takes a literal context of its own,
200
+
on any derive: every output of a `plaintext = T` record shares the caller's.
201
+
A field whose type is itself a record is an ordinary field: it is sealed under
202
+
`<context>/<field>`, and its own contexts are extended by that pair. A struct
203
+
derive takes one output per plaintext field; to store several terms from one
204
+
field, type that field `Encrypted<Terms>`. The context is the AAD of every
205
+
stored ciphertext in the column, so renaming a plaintext *field* is still a
206
+
data migration: pin the old name first with `identity = ".."`.
207
+
208
+
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
209
204
210
### Relationship to `Encrypt`
205
211
@@ -252,7 +258,7 @@ vitaminc already has the generic notion, twice — `Aad<'a>` (aead) and `PrfCont
252
258
253
259
Context is threaded **per value**, as an argument. It is not baked into the cipher.
254
260
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.
261
+
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>`), 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 (every field of a `plaintext = T` record). 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
262
257
263
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