mesh: shuffle offered inventory so its order isn't a stable fingerprint - #93
Conversation
envelopeIds() returns storage order, which on the sqlite path is expiry-correlated (envelopes_by_expiry). A stable order is a fingerprint, and the MAX_SYNC_PER_PEER cap then emits the same soonest-to-expire subset every offer. Both let a peer that connects twice across a BLE identifier rotation correlate the two sessions by the offered set. Shuffle before the cap: fresh random sample per offer, no stable order. Reconciliation is set membership, so order carries no meaning.
commitchan
left a comment
There was a problem hiding this comment.
Good catch on the fingerprint surface. envelopeIds() comes back in storage order, which on the sqlite path is expiry-correlated via envelopes_by_expiry — so both the offered order and the soonest-to-expire prefix under the cap were stable across encounters, i.e. linkable. A fresh random sample per offer kills both.
The shuffle is a correct Fisher–Yates over randomBytes (CSPRNG, not Math.random), guarded for <2, and it mutates a fresh array — both envelopeIds() implementations return a freshly-mapped array, so nothing shared is clobbered. Crucially it doesn't touch correctness: handleInventory reconciles on set membership, so offer order carries no meaning, and sampling-before-the-cap actually gives a large cache better coverage across reconnects than the old deterministic prefix. 231 tests green (inventory-sync included), typecheck and lint clean.
Only nit, non-blocking: r[i] % (i+1) has a little modulo bias, but it's a uniform skew across all devices and the order still varies every call, so it doesn't reintroduce a fingerprint. Fine as is.
Merging. (Rebases onto current main, which has the Expo dep fix your base predates.)
offerInventory() sends envelopeIds() in storage order. On the sqlite path that order is expiry-correlated (envelopes_by_expiry index), and once the cache exceeds MAX_SYNC_PER_PEER the cap emits the same soonest-to-expire subset on every offer.
A stable order and a stable capped subset make the offered id set a slowly-varying fingerprint: a peer that connects twice across a BLE identifier rotation can correlate both sessions by the set it was offered, which works against the point of rotating the advertising identity.
Shuffle before the cap so each offer is a fresh random sample with no stable order. Reconciliation is set membership (handleInventory), so order is irrelevant to correctness.
Tests: full suite passes (231), typecheck clean.