Skip to content

Commit 704a50a

Browse files
authored
Merge pull request #1455 from cipherstash/chore/merge-main-into-next-1031
chore: merge main into next
2 parents ffeed7a + 5697451 commit 704a50a

74 files changed

Lines changed: 2911 additions & 1460 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.cargo/mutants.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ exclude_re = [
6363
# `a_builder_without_a_transport_is_refused_when_there_is_no_bundled_one`.
6464
'stack-auth/src/error\.rs:\d+:\d+: replace RequestError::is_no_transport -> bool with false$',
6565
'stack-auth/src/auto_strategy\.rs:150:9: replace AutoStrategy::detect_inner -> Result<Self, AuthError> with Ok\(Default::default\(\)\)$',
66-
'stack-auth/src/token_store\.rs:(258|266):9: replace <impl TokenStore for TokenStoreFn<L, S>>::(load|save)',
66+
'stack-auth/src/token_store\.rs:(261|269):9: replace <impl TokenStore for TokenStoreFn<L, S>>::(load|save)',
6767
# stack-auth — equivalent.
6868
'stack-auth/src/(access_key|oidc)_refresher\.rs:\d+:\d+: replace <impl Refresher for \w+(<P>)?>::try_credential -> Option<Self::Credential> with Some\(Default::default\(\)\)$',
6969
'stack-auth/src/error\.rs:\d+:\d+: replace AuthErrorKind::payload -> serde_json::Map<String, serde_json::Value> with Default::default\(\)$',
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cipherstash/auth': patch
3+
---
4+
5+
Keep native token refreshes running when their caller is cancelled, so rotated device-session credentials are saved and subsequent requests can still authenticate. Close a refresh-waiter notification race, and give the caller that started a refresh the new token rather than the expiring one it replaced.

‎.changeset/eql-3-1-0-migration.md‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
'@cipherstash/stack-prisma': minor
33
---
44

5-
Move the bundled EQL v3 migrations to **eql-3.1.0**. EQL 3.1.0 is a minor
6-
release for the Rust `eql-bindings` crate. Its install SQL changes only the
7-
version stamp: `eql_v3.version()` returns `'3.1.0'` and the `eql_v3` schema
8-
comment reads `'3.1.0'`. No function, operator, domain or index behaviour
9-
changes.
5+
Move the bundled EQL v3 migrations to **eql-3.1.0**. Its install SQL changes
6+
the version stamp (`eql_v3.version()` returns `'3.1.0'` and the `eql_v3`
7+
schema comment reads `'3.1.0'`) and the ORE block comparator behind every
8+
ordered ORE operator and the ORE operator class. The comparator now runs a
9+
constant number of operations whatever two values share, closing a timing
10+
leak. Ordering results are unchanged for well-formed values, but a block-ORE
11+
text value with a malformed or mismatched-width term now raises on every
12+
comparison against it. Check your `_ore` text columns before migrating; see
13+
[U-004](../../../../packages/eql/docs/upgrading/v3.1.md#u-004-block-ore-text-comparisons-check-every-term).
14+
No domain, wire format or index term changes.
1015

1116
Two artefacts carry the new bundle:
1217

@@ -52,4 +57,6 @@ baseline. The team chose it on these numbers: `@cipherstash/stack-prisma` had
5257
481 npm downloads in the 30 days to 3 October 2026, and 384 in the last 7 of
5358
those days, 305 of them on the 1.2.0 release day. The append-only alternative,
5459
a second `from: null` genesis edge, would add another permanent ~2.6 MB copy of
55-
the bundle for a release that changes no SQL behaviour.
60+
the bundle. That choice was made when 3.1.0 changed only the version stamp; the
61+
comparator change came after it and does not alter the trade, since every
62+
database reaches the same bundle either way.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@cipherstash/eql': minor
3+
---
4+
5+
Security: the ORE block comparator (`eql_v3_internal.compare_ore_block_256_term`
6+
and `compare_ore_block_256_terms`, behind every ordered ORE operator and the ORE
7+
operator class) now runs a constant number of operations for operands of the
8+
same shape (term count, term width, NULL positions), whether and wherever they
9+
differ. Previously its run time depended on where two values first differ, so a
10+
party able to time queries could learn how long a prefix their query value
11+
shares with stored values. For text the effect was large: a comparison took
12+
about 11 µs when the first term differs and 47 µs when only the sixth does.
13+
Results are unchanged. Single-term comparisons cost about 6% more when the
14+
terms differ and about 36% more (roughly 1 µs) when they are equal, since equal
15+
terms now run the hash too; text comparisons now take about 18 µs whatever the
16+
operands. Multi-term (text) values now check every paired term, so a
17+
malformed or mismatched-width term after the first difference, or one paired
18+
with a NULL element, raises instead of being skipped; see
19+
[U-004](../../docs/upgrading/v3.1.md#u-004-block-ore-text-comparisons-check-every-term)
20+
for how to find such rows before upgrading. A timing harness is in
21+
`packages/eql/tests/timing/ore_block_256/`.

‎.changeset/eql-v4-block-ore-types.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@cipherstash/eql': minor
33
---
44

5-
**EQL v4 compares ore-rs's 6-bit block ORE, and Stack Encrypt produces the block ORE types `IntegerOrdOre`, `SmallintOrdOre`, `BigintOrdOre`, `RealOrdOre`, `DoubleOrdOre`, `TextOrdOre` and `TextSearchOre`.** A v4 `ob` term is one ore-rs ciphertext: the fixed-width scheme for numbers, the chained scheme for text (padded to a power-of-two length bucket of at least 16 bytes). The v4 bundle names its block ORE objects `ore_block_v2` (`eql_v4_internal.ore_block_v2_term`, `compare_ore_block_v2_term`) and compares them with a new comparator that checks each term's shape, raises on malformed or mismatched terms, and runs a constant operation count for terms of one shape. The EQL v3 bundle is unchanged, byte for byte: its `ob` is still cipherstash-client's 8-bit block ORE under `ore_block_256`. See [U-002](../../docs/upgrading/v3.1.md#u-002-the-block-ore-term-is-ore-rss-6-bit-scheme): the comparator refuses the 8-bit terms the previous `eql_v4` bundle compared, so rows or operands holding them must be written again through Stack Encrypt.
5+
**EQL v4 compares ore-rs's 6-bit block ORE, and Stack Encrypt produces the block ORE types `IntegerOrdOre`, `SmallintOrdOre`, `BigintOrdOre`, `RealOrdOre`, `DoubleOrdOre`, `TextOrdOre` and `TextSearchOre`.** A v4 `ob` term is one ore-rs ciphertext: the fixed-width scheme for numbers, the chained scheme for text (padded to a power-of-two length bucket of at least 16 bytes). The v4 bundle names its block ORE objects `ore_block_v2` (`eql_v4_internal.ore_block_v2_term`, `compare_ore_block_v2_term`) and compares them with a new comparator that checks each term's shape, raises on malformed or mismatched terms, and runs a constant operation count for terms of one shape. This change leaves the EQL v3 bundle as it was: its `ob` is still cipherstash-client's 8-bit block ORE under `ore_block_256`. See [U-002](../../docs/upgrading/v3.1.md#u-002-the-block-ore-term-is-ore-rss-6-bit-scheme): the comparator refuses the 8-bit terms the previous `eql_v4` bundle compared, so rows or operands holding them must be written again through Stack Encrypt.

‎.changeset/eql-v4-order-term-shape.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@cipherstash/eql': minor
33
---
44

5-
**An `eql_v4` order term is one hex string, and every `eql_v4` domain checks its shape when it is written.** A block ORE term (`ob`, and the `lo` and `hi` bounds of a block ORE range) is the lowercase hex of the whole ore-rs ciphertext, `"ob": "0202…"`, where it was a one-element array. Every `eql_v4` domain carrying `ob` must now hold a string starting `0202` or `0203`, and one carrying `op` a string starting `00` or `01`: an EQL v3 term or a term of the wrong scheme is refused with `check_violation` at the write or cast, not at the first comparison. The check reads only the prefix, so its cost does not grow with the term. In `eql-bindings`, a v4 `ob` field is Stack Encrypt's `OreBlockTerm` and an `op` field its `OpeTerm`, typed by the plaintext they order (`DateOrdOre::ob` is an `OreBlockTerm<Date>`), and reading a payload checks the term's structure. The `eql_v3` bundle is unchanged, byte for byte. A database that installed an earlier `eql_v4` bundle keeps its stored domains across an install, so it must drop its stored order domains first: see [U-003](../../docs/upgrading/v3.1.md#u-003-an-order-term-is-one-hex-string-checked-when-it-is-written).
5+
**An `eql_v4` order term is one hex string, and every `eql_v4` domain checks its shape when it is written.** A block ORE term (`ob`, and the `lo` and `hi` bounds of a block ORE range) is the lowercase hex of the whole ore-rs ciphertext, `"ob": "0202…"`, where it was a one-element array. Every `eql_v4` domain carrying `ob` must now hold a string starting `0202` or `0203`, and one carrying `op` a string starting `00` or `01`: an EQL v3 term or a term of the wrong scheme is refused with `check_violation` at the write or cast, not at the first comparison. The check reads only the prefix, so its cost does not grow with the term. In `eql-bindings`, a v4 `ob` field is Stack Encrypt's `OreBlockTerm` and an `op` field its `OpeTerm`, typed by the plaintext they order (`DateOrdOre::ob` is an `OreBlockTerm<Date>`), and reading a payload checks the term's structure. This change leaves the `eql_v3` bundle as it was. A database that installed an earlier `eql_v4` bundle keeps its stored domains across an install, so it must drop its stored order domains first: see [U-003](../../docs/upgrading/v3.1.md#u-003-an-order-term-is-one-hex-string-checked-when-it-is-written).

‎.changeset/eql-v4-text-search-optional-terms.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@cipherstash/eql': minor
33
---
44

5-
**An EQL v4 text search query carries only the terms its operator reads.** `eql_v4.query_text_search` and `eql_v4.query_text_search_ore` now require at least one of their terms (`hm`, `op` or `ob`, `bf`) instead of all of them, so `col = $1` and `col < $1` work on text too short to cut a 3-gram, which has no match term. Each extractor over such a query raises (`invalid_parameter_value`) wherever it reads the operand when the query lacks the term it reads, or holds it as JSON `null` or an empty array, naming the domain, the term and the operators that compare it: `=` without `hm`, `<` without `op`, `@@` without `bf`. So a missing term raises rather than matching no row silently, or every row; the raise is at evaluation, so a generic plan that never reaches the predicate returns no row and no error. Stored `eql_v4_text_search` values still carry every term. The `eql_v3` domains are unchanged: the v3 bundle is byte for byte what it was. With it, **Stack Encrypt produces `TextSearch`** (`eql-bindings`, `stack-encrypt` feature): `TextSearchQuery`'s terms are `Option`s, built per index through `TextSearchEqualityQuery`, `TextSearchOrderQuery` and `TextSearchMatchQuery`, and the stored value carries every term: text too short for a 3-gram stores the empty filter (`"bf": []`, the new `v4::terms::SearchBloomFilter` field type), so `=` and `<` find it and no `@@` query does, where `TextMatch` still refuses it. `TextSearchQuery`'s `Option` terms and `TextSearch`'s `SearchBloomFilter` change the fields of the unreleased `v4` module only; no published `eql-bindings` has it. See [U-001](../../docs/upgrading/v3.1.md#u-001-a-text-search-query-carries-only-the-terms-its-operator-reads) for what a hand-built query must carry, the raise, and the rollback.
5+
**An EQL v4 text search query carries only the terms its operator reads.** `eql_v4.query_text_search` and `eql_v4.query_text_search_ore` now require at least one of their terms (`hm`, `op` or `ob`, `bf`) instead of all of them, so `col = $1` and `col < $1` work on text too short to cut a 3-gram, which has no match term. Each extractor over such a query raises (`invalid_parameter_value`) wherever it reads the operand when the query lacks the term it reads, or holds it as JSON `null` or an empty array, naming the domain, the term and the operators that compare it: `=` without `hm`, `<` without `op`, `@@` without `bf`. So a missing term raises rather than matching no row silently, or every row; the raise is at evaluation, so a generic plan that never reaches the predicate returns no row and no error. Stored `eql_v4_text_search` values still carry every term. This change leaves the `eql_v3` domains as they were. With it, **Stack Encrypt produces `TextSearch`** (`eql-bindings`, `stack-encrypt` feature): `TextSearchQuery`'s terms are `Option`s, built per index through `TextSearchEqualityQuery`, `TextSearchOrderQuery` and `TextSearchMatchQuery`, and the stored value carries every term: text too short for a 3-gram stores the empty filter (`"bf": []`, the new `v4::terms::SearchBloomFilter` field type), so `=` and `<` find it and no `@@` query does, where `TextMatch` still refuses it. `TextSearchQuery`'s `Option` terms and `TextSearch`'s `SearchBloomFilter` change the fields of the unreleased `v4` module only; no published `eql-bindings` has it. See [U-001](../../docs/upgrading/v3.1.md#u-001-a-text-search-query-carries-only-the-terms-its-operator-reads) for what a hand-built query must carry, the raise, and the rollback.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@cipherstash/stack": patch
3+
---
4+
5+
Fix the README's OIDC federation example, which passed the `Result` from `OidcFederationStrategy.create()` straight to `config.authStrategy` and failed in `Encryption()` with `opts.strategy.getToken is missing`.
6+
It now unwraps the `Result` and passes `strategy.data`, and so does the `LockContext` JSDoc example.

‎.github/dependabot.yml‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,10 @@ updates:
249249

250250
# ── Cargo (the root workspace — the stack-* crates) ─────────────
251251
# The root workspace (the six stack-* crates and the three node binding
252-
# crates) and the five detached workspaces beside it: the three cargo-fuzz
253-
# crates and the two Go WASI guests. Each has its own Cargo.lock, and each
254-
# is its own workspace root, so each is listed.
252+
# crates) and the six detached workspaces beside it: the three cargo-fuzz
253+
# crates, the two Go WASI guests and the EQL ORE timing harness's ciphertext
254+
# generator. Each has its own Cargo.lock, and each is its own workspace
255+
# root, so each is listed.
255256
- package-ecosystem: cargo
256257
directories:
257258
- /
@@ -260,6 +261,7 @@ updates:
260261
- /packages/stack-encrypt/fuzz
261262
- /languages/golang/encrypt/guest
262263
- /languages/golang/auth/guest
264+
- /packages/eql/tests/timing/ore_block_256/ctgen
263265
# Lockfile only, like the other two cargo entries. Every one of these
264266
# workspaces shares crates by path with another: the fuzz crates and both
265267
# guests depend on root-workspace crates, the encrypt guest also on
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Auth refresh cancellation soak
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
long:
7+
type: boolean
8+
default: false
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
soak:
15+
runs-on: blacksmith-16vcpu-ubuntu-2204
16+
timeout-minutes: 30
17+
env:
18+
MISE_ENV: test
19+
NEXTEST_PROFILE: ci
20+
steps:
21+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
22+
with:
23+
persist-credentials: false
24+
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
25+
with:
26+
version: 2026.4.0
27+
install: true
28+
working_directory: .
29+
cache: true
30+
cache_key_prefix: mise-v1-ubuntu-2204
31+
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
32+
- name: Short seeded soak
33+
if: ${{ !inputs.long }}
34+
run: cargo nextest run --locked -p stack-auth --all-features -E 'test(cancellation_soak_short)' --success-output final
35+
- name: Long seeded soak
36+
if: inputs.long
37+
run: mise run test:soak:stack-auth

0 commit comments

Comments
 (0)