release(v4.11.0): streaming Double-Ratchet sub-sessions (ShadeStream)
Some checks failed
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Test / test (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled

Answers Vyvern FR shade-ws-streaming-ratchet.md with a first-class
streaming-session API rather than the documented-contract fallback.
The Double-Ratchet crypto was already safe for high-frequency
one-directional use; the send/receive wrapper was not (per-frame
saveSession keystore write; shared per-peer mutex + single stored
session row coupling reuse to the HTTP path).

- @shade/core: stream.ts — identity-bound 3-DH seeding (X3DH-minus-
  prekeys, no prekey-server round trip, mutually authenticated against
  the parent session's pinned identities), bootstrapStreamSession
  reusing init{Sender,Receiver}Session verbatim, in-memory-only
  StreamRatchet (own op-mutex, never persisted, zeroized on close).
  beginStream/acceptStream on ShadeSessionManager; Stream{Closed,
  Handshake}Error; stream.opened/closed events.
- @shade/proto: STREAM_OPEN/OPEN_ACK/FRAME wire (0x31/0x32/0x33),
  additive; inspectEnvelopeType extended.
- @shade/sdk: Shade.openStream/acceptStream → ShadeStream
  (handshakeFrame/handleHandshake/seal/open/close), transport-
  agnostic, independent of encrypt/decrypt queues + parent session,
  identical server (sqlite:) and browser (IndexedDB) — touches no
  storage.
- Tests: 5000-frame one-directional burst (bounded skipped keys + FS
  zeroize), parent-session independence, replay/rewind rejection,
  mutual-auth, proto wire round-trips. Full suite green (1159 pass).
- docs/streaming-sessions.md (R1–R7 contract); SECURITY.md matrix rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 11:29:09 +02:00
parent 188c3db56a
commit 037f994572
39 changed files with 1241 additions and 31 deletions

View File

@@ -5,6 +5,60 @@ All notable changes to Shade are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.11.0] — 2026-05-15 — Streaming Double-Ratchet sub-sessions
Answers Vyvern FR `shade-ws-streaming-ratchet.md` (the last Phase-2
blocker) with a first-class streaming-session API rather than the
"documented contract" fallback: the Double-Ratchet crypto was already
safe for high-frequency one-directional use; the `send`/`receive`
*wrapper* was not (a `saveSession` keystore write per frame; a shared
per-peer mutex + single stored session row coupling any reuse to the
HTTP path). `ShadeStream` keeps the proven ratchet and fixes the
wrapper.
**`@shade/core`**
- New `stream.ts`: `deriveStreamRootKey` (identity-bound 3-DH —
X3DH-minus-prekeys, no prekey-server round trip; mutually
authenticated against the parent session's already-pinned
identities), `bootstrapStreamSession` (reuses
`initSenderSession`/`initReceiverSession` verbatim), and
`StreamRatchet` — an in-memory `seal`/`open`/`close` holder on its
own op-mutex, **never persisted**, zeroized on close.
- `ShadeSessionManager.beginStream` / `acceptStream` custody the
identity keys for the handshake without exposing private material;
both require an established parent session (no first-contact).
- New `StreamClosedError` / `StreamHandshakeError`; `stream.opened` /
`stream.closed` events.
**`@shade/proto`**
- Wire types `STREAM_OPEN` (0x31), `STREAM_OPEN_ACK` (0x32),
`STREAM_FRAME` (0x33) with encode/decode + `inspectEnvelopeType`
extension. A `STREAM_FRAME` carries one ratchet message via the exact
inner codec the HTTP path uses — one sealed frame ⇒ one WS frame.
**`@shade/sdk`**
- `Shade.openStream(peer)` / `Shade.acceptStream(peer, openBytes)`
returning `ShadeStream` (`handshakeFrame` / `handleHandshake` /
`seal` / `open` / `close`). Transport-agnostic like `send`/`receive`;
auto-establishes the parent session if missing. Independent of the
per-peer encrypt/decrypt queues and the stored parent session (R5).
Identical on the `sqlite:` server build and the IndexedDB browser
build (R4) — it touches no storage at all.
**Security / perf**
- Per-frame cost is exactly one symmetric KDF + one AES-GCM (no
keystore I/O) — strictly better than the budgeted "doubled CPU".
In-memory-only is a forward-secrecy property, not a shortcut; a
dropped stream is re-opened, never resumed.
- New `docs/streaming-sessions.md` (full R1R7 contract); SECURITY.md
threat-matrix rows added with tests
(`packages/shade-core/tests/stream.test.ts`,
`packages/shade-proto/tests/stream-wire.test.ts`).
## [Unreleased — 2026-05-09] — Android: V4.9/V4.10 ports + KeystoreStorage adapter
The Kotlin side of the v4.10 cross-host approval routing FR. With this