Skip to content

Commit f915cf5

Browse files
committed
feat(stack-encrypt)!: encrypt order terms through cllw-ore's vitaminc schemes
cllw-ore 0.5.2-rc.1 implements vitaminc-ore's `OrderScheme` behind its `vitaminc` feature, so Stack Encrypt no longer encodes anything for ordering itself. An ORE or OPE term is now cllw-ore's `vitaminc::Ore` or `vitaminc::Ope` encryption of the value: the engine names the value's order target through the new sealed `sem::Orderable` (integers at their own width, `f32`/`f64` as `F64`, text as `Text`, bytes as `Bytes`) and passes the value straight through; cllw-ore encodes it with vitaminc-ore, text by the system ADR-0002's fold. That replaces the earlier workaround, which built text's order key here and handed the folded bytes to cllw-ore 0.5 through `Any` downcasts: `OrderSource`, `text_order_key` and the downcasts are gone. The per-index key derivation is kept: the PRF block keys the scheme inside the visitor and is wiped there, and the column is opened with the value's target and the empty context, since the key is already the index's own. The key labels move to `/v2` because every order term changes bytes (new column salt, cllw-ore's marker byte for text and bytes). Nothing was deployed (ADR-0002); the pins and both fixtures are re-recorded. Every source type's term is now one ciphertext type (`OreCllw8VariableV1`, `OpeCllw8VariableV1`); `OreTerm<T>`/`OpeTerm<T>` keep `T` as a marker so typed callers compile unchanged. `Between` judges an inverted range in the order the stored terms compare, by encrypting both bounds with OPE under a fixed comparison key and comparing the bytes, on the typed and the data-plan path alike. Claude-Session: https://claude.ai/code/session_01BZhkvKLpNtKuzj3HCfMXoL
1 parent 300d307 commit f915cf5

23 files changed

Lines changed: 465 additions & 422 deletions

‎Cargo.lock‎

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ stack-profile = { path = "./packages/stack-profile", version = "0.43.0" }
8181
cts-common = { version = "0.43.0", default-features = false }
8282
zerokms-protocol = "0.12.31"
8383
recipher = "=0.3.1"
84-
cllw-ore = { version = "=0.5.0", default-features = false }
84+
# cllw-ore's `vitaminc` feature implements vitaminc-ore's `OrderScheme`,
85+
# which Stack Encrypt's order terms are encrypted through. It first ships
86+
# in a pre-release, and Cargo resolves a pre-release only when named.
87+
cllw-ore = { version = "=0.5.2-rc.1", default-features = false, features = ["vitaminc"] }
8588

8689
# External dependencies, with the suite's feature lists.
8790
base64 = "0.22.0"

‎languages/golang/encrypt/guest/Cargo.lock‎

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎languages/golang/encrypt/guest/tests/native_ops.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,15 +327,16 @@ fn guest_terms_match_the_native_sem_derivations() {
327327
.expect("native");
328328
assert_eq!(eq_bytes, native_bytes.as_bytes());
329329

330-
// Variable-width CLLW output for strings.
330+
// Variable-width CLLW output for strings: the text behind cllw-ore's
331+
// one marker byte, eight bytes a plaintext byte.
331332
let ore_s = block_on(ops::term(
332333
&cipher.default_keyset(),
333334
&encode(s("alice")),
334335
&encode(s("users/name")),
335336
TERM_ORE,
336337
))
337338
.expect("string ore");
338-
assert_eq!(ore_s.len(), 5 * 8);
339+
assert_eq!(ore_s.len(), 6 * 8);
339340

340341
// No ZeroKMS traffic for any of it.
341342
assert_eq!(cipher.kms().generate_calls.load(Ordering::SeqCst), 0);

‎packages/eql/Cargo.lock‎

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/stack-encrypt/CHANGELOG.md‎

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -159,19 +159,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
159159

160160
### Breaking
161161

162-
- **Text orders by its folded order key.** An ORE or OPE term over text is
163-
derived over the system ADR-0002's fold (Unicode 16 NFC with case and
164-
accents folded, from `vitaminc-ore`) in place of cllw-ore's own
165-
collation, under a key framing that names the fold's frozen label. So
166-
`Café`, `cafe`, `CAFÉ` and `cafe` with a combining accent are one term and
167-
tie, `alice` orders before `Zoe`, and `é` orders with `e`, not after `z`.
168-
Every text order term changes bytes; integer, float and bytes order terms
169-
do not. None was deployed (ADR-0002). Text holding a code point Unicode 16
170-
does not assign has no fold and no order term (`TermError::Normalize`),
171-
on write and on query. The ordering operators are answered on text, on
172-
the typed path (`op::Ordered` for `String`, whose `Between` judges an
173-
inverted range on the folded keys: `("b", "A")` is inverted, `("A", "a")`
174-
is one value) and on the data-plan path, so a binding generates them too.
162+
- **Order terms encrypt through cllw-ore's vitaminc schemes, and text is
163+
ordered.** An ORE or OPE term is now cllw-ore 0.5.2-rc.1's
164+
`vitaminc::Ore`/`vitaminc::Ope` encryption of the value's `vitaminc-ore`
165+
order encoding: the engine passes the value through and names its type's
166+
target (`sem::Orderable`: integers at their own width, `f32`/`f64` as
167+
`F64`, text as `Text`, bytes as `Bytes`), and cllw-ore does the encoding.
168+
Text is ordered by the system ADR-0002's fold (Unicode 16 NFC with case
169+
and accents folded), so `Café`, `cafe`, `CAFÉ` and `cafe` with a
170+
combining accent are one term and tie, `alice` orders before `Zoe`, and
171+
`é` orders with `e`, not after `z`. Every order term changes bytes: the
172+
key labels move to `/v2`, the column salt binds the scheme and the
173+
target, and text and bytes go in behind cllw-ore's marker byte. None was
174+
deployed (ADR-0002). Every source type's term is one ciphertext type,
175+
`OreCllw8VariableV1` or `OpeCllw8VariableV1`; `OreTerm<T>` and
176+
`OpeTerm<T>` keep `T` to name the source, and `new`, `inner` and
177+
`into_inner`, `KeysetCipher::ore_term` and `ope_term` take or return those
178+
types. `sem::CllwOreEncrypt` and `sem::CllwOpeEncrypt` are no longer
179+
re-exported, and the term bounds are `Orderable`. Text holding a code
180+
point Unicode 16 does not assign has no order term
181+
(`TermError::Normalize`), on write and on query. The ordering operators
182+
are answered on text, on the typed path (`op::Ordered` for `String`) and
183+
on the data-plan path, so a binding generates them too. `Between` judges
184+
an inverted range in the order the stored terms compare, by comparing the
185+
bounds' encryptions: `("b", "A")` is inverted, `("A", "a")` is one value.
175186

176187
- **A match value with no tokens is stored as the empty filter, so
177188
`KeysetCipher::match_terms` is no longer a query.** In 0.2.0 the same call
@@ -215,9 +226,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
215226
way. Every equality and match term changes
216227
bytes (the domains move to `/v2`); ORE and OPE terms do not. No stored
217228
term existed to keep; the record fixture is regenerated. Text order
218-
terms derive over the folded order key (see "Text orders by its folded
219-
order key" above), so equality and ordering agree on two forms of
220-
`café`.
229+
terms are encoded in the same normal form, folded further (see "Order
230+
terms encrypt through cllw-ore's vitaminc schemes" above), so equality
231+
and ordering agree on two forms of `café`.
221232
- **`dynamic::Value` and `dynamic::Scalar` are gone.** A field lowered from
222233
data holds an `FfiValue`, which is now `Clone` (a deep copy into fresh
223234
`Protected` payloads, as `Value`'s was) and reads as a context label.

‎packages/stack-encrypt/Cargo.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ vitaminc-hmac = { workspace = true }
3737
# number whatever its width, text in Unicode 16 NFC. Match text is
3838
# normalized by the same crate before it is tokenized.
3939
vitaminc-normalize = { workspace = true }
40-
# Text order terms are derived over `vitaminc-ore`'s folded order key, not
41-
# cllw-ore's own text collation.
40+
# The order targets (`OrderTarget`) and the scheme traits cllw-ore's
41+
# `vitaminc` feature implements, which order terms encrypt through.
4242
vitaminc-ore = { workspace = true }
4343
vitaminc-prf = { workspace = true }
4444
vitaminc-protected = { workspace = true }

‎packages/stack-encrypt/docs/adr/0009-operators-select-the-index-and-a-query-term-is-not-the-stored-term.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -500,10 +500,10 @@ key tie; that is what the column declared, and an earlier draft of this ADR
500500
was wrong to call it a false positive and put the term under `Probabilistic`.
501501
Its use is ordering and cursor paging on names (`ORDER BY name`, "the page
502502
after Nguyen"), and EQL v4 carries `query_text_ord` and `query_text_ord_ore`
503-
for it. Stack Encrypt produces the term: the order key is vitaminc-ore's
504-
text order encoding, the system ADR-0002's fold pinned to Unicode 16, which
505-
vitaminc 0.6 ships, and the ORE and OPE indexes answer the orderings on text
506-
over it. `TextOrdOre` and `TextSearchOre` also need the block ORE scheme EQL
503+
for it. Stack Encrypt produces the term: it hands the text to cllw-ore's
504+
vitaminc schemes as the `Text` target, whose order key is vitaminc-ore's
505+
text order encoding, the system ADR-0002's fold pinned to Unicode 16, and
506+
the ORE and OPE indexes answer the orderings on text over it. `TextOrdOre` and `TextSearchOre` also need the block ORE scheme EQL
507507
stores (#1132).
508508

509509
A `Ranked` term's `target_limit` applies the index's configured recall

‎packages/stack-encrypt/fuzz/Cargo.lock‎

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/stack-encrypt/src/dynamic/query.rs‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ use crate::op::{Limit, Stats};
7777
use crate::sem::{match_limit, MatchOptions, QueryTokens, TermError};
7878
use crate::target::IndexSpec;
7979
use crate::{KeysetCipher, Pending};
80+
use vitaminc_ore::OrderTarget;
8081

8182
/// An operator, named at run time: the runtime form of the
8283
/// [`op`](crate::op) module's operator types, as [`IndexSpec`] is of the
@@ -582,7 +583,7 @@ fn check_input(index: &IndexSpec, input: &Input) -> Result<(), Error> {
582583
// derivation would refuse it after one.
583584
(IndexSpec::Ore | IndexSpec::Ope, Input::One(FfiValue::String(text))) => {
584585
if let Some(text) = utf8(text) {
585-
crate::sem::text_order_key(text)
586+
crate::sem::order_cmp_as(OrderTarget::Text, text, text)
586587
.map(drop)
587588
.map_err(refused)?;
588589
}
@@ -602,18 +603,15 @@ fn check_input(index: &IndexSpec, input: &Input) -> Result<(), Error> {
602603
}
603604

604605
/// The order of a range's two bounds, as the server compares their terms:
605-
/// integers of one kind by value, text by its order key, so `("b", "A")` is
606-
/// inverted and `("A", "a")` is one value. `None` for anything else.
606+
/// integers of one kind by value, text in cllw-ore's order of its encoding
607+
/// (the system ADR-0002's fold), so `("b", "A")` is inverted and
608+
/// `("A", "a")` is one value. `None` for anything else.
607609
fn bound_order(lo: &FfiValue, hi: &FfiValue) -> Result<Option<Ordering>, TermError> {
608610
if let (FfiValue::String(lo), FfiValue::String(hi)) = (lo, hi) {
609611
let (Some(lo), Some(hi)) = (utf8(lo), utf8(hi)) else {
610612
return Ok(None);
611613
};
612-
let (lo, hi) = (
613-
crate::sem::text_order_key(lo)?,
614-
crate::sem::text_order_key(hi)?,
615-
);
616-
return Ok(Some(lo.as_slice().cmp(hi.as_slice())));
614+
return crate::sem::order_cmp_as(OrderTarget::Text, lo, hi).map(Some);
617615
}
618616
Ok(integer_order(lo, hi))
619617
}

0 commit comments

Comments
 (0)