Skip to content

2026-07-23: E2EE to-device mailbox relationship gate (M-E2EE-6) โ€‹

What shipped โ€‹

  • POST /auth/key-directory/to-device now refuses a deposit unless the sender and the recipient share a connections doc (checked both participant orders, ANY status), via a new hasConnectionBetween helper in keyDirectory.js. A no-connection drop is reported as M_FORBIDDEN, NON-retryable: the client acks it without re-sharing (retrying cannot help a stranger deposit), while rate-limit drops stay retryable so real key shares fail closed as before. Self-deposits skip the gate (your own mailbox is always yours; the multi-device future shares keys to your other devices this way).
  • planToDeviceWrites accepts an object verdict ({allowed, errcode, retryable}) from the per-recipient gate alongside the legacy boolean, so drop class is decided at the gate.

Why the June deferral concerns dissolved โ€‹

  1. "Needs a composite index": it does not. The gate is equality-only + limit(1), served by Firestore's merged single-field indexes. A same-shaped two-way query (with a third equality, so a fortiori) already runs live in the wave flow (wave.service.js hasActiveConnection), which is the empirical proof on dev.
  2. "Risks dropping a legit key share on a timing edge": the sole origin of a to-device key share is message-send (shareRoomKey inside encryptMessage), which requires a settled connectionId; acceptWave writes the connection doc and performs no encryption at accept time. The gate is deliberately status-blind (doc existence, not status == 'active'), so an archived (met/expired) chat a peer reopens still re-keys until the 90-day idle purge deletes the doc, which is what finally closes the pair's channel.

Forward-note (recorded in code) โ€‹

Bonfire Megolm group sessions (CHAT_ENCRYPTION.md section 5, deferred until bonfires ship) distribute group keys over these same pairwise channels, and bonfire members need NOT be pairwise-connected. When bonfires land, this gate must learn a second predicate (shared bonfire membership) or bonfire key delivery will be dropped here.

Verified โ€‹

  • 10 new tests in keyDirectory.test.js (28 total in the file): either-order hit, stranger miss, status-blind archived hit, M_FORBIDDEN non-retryable batch semantics (sibling writes survive, batch not marked retryable), mixed forbidden + rate-limit drops keeping the batch retryable, and the makeToDeviceGate composition set (stranger refused before the limiter, self-deposit skips the lookup, per-device cost charged, limiter denial defaults retryable, forgotten-flag permanent denial defaults non-retryable).
  • npm run validate -- --workspace services/api/auth green (15/15 including the full auth suite).
  • No client change needed: firestoreTransport.js already acks non-retryable failures without re-sharing (WI-2 contract).

Pre-PR adversarial review findings (applied) โ€‹

  • Retryable default hardened: an object verdict that omits retryable now defaults from its errcode (transient only for M_LIMIT_EXCEEDED), so a future permanent-denial producer (the bonfire predicate) that forgets the flag cannot default into the client's fail-closed re-share loop. Regression test added.
  • Gate extracted as makeToDeviceGate (injectable limiter) and the previously untested wiring covered: stranger refused before the limiter is consulted, self-deposit skips the relationship lookup, connected recipient still charged at per-device cost.
  • Ordering validated: gate-before-limiter is cheaper AND cleaner (the durable limiter is itself a Firestore transaction; limiter-first would cost a read + write and litter a rate-bucket doc per stranger deposit, gate-first costs two reads and writes nothing).
  • Pre-existing rules softness filed as #700: a connection doc's provenance is a real wave to the sender, but rules do not yet require the wave to be ACCEPTED, so a declined wave's recipient can still open the thread + key channel inside the wave's 7-day TTL. Separate PR (review-sensitive rules surface).

Tracker updates in this PR โ€‹

  • SECURITY_REMEDIATION.md: M-E2EE-6 next-iteration item marked DONE with verification.
  • 2026-06-16-branch-review-remediation.md: WI-10 part 2 deferral resolved.

Built with VitePress