Message size is visible on every envelope, and is not on /metadata #59

Open
opened 2026-09-07 13:32:10 +00:00 by paulhitt · 2 comments
paulhitt commented 2026-09-07 13:32:10 +00:00 (Migrated from gitlab.com)

Split out of #18, which asked what an operator watching the envelopes table for a week could reconstruct. This is the part of the answer that is a change rather than a note.

The finding

payload is opaque ciphertext, which is what /metadata says. Its length is not opaque, and nothing hides it. Measured on production 2026-09-07:

select length(payload), count(*) from envelopes group by 1 order by 1;

Fourteen envelopes, fourteen distinct lengths, clustering by kind:

bytes envelopes
~6.3 K 2
~9.3–9.4 K 6
~13.9 K 4
~16.7 K 2

An operator reading the table sees each message's size to the byte, and sizes group by kind. A shape is a guess about content: a receipt is not the size of a photo, and a one-word reply is not the size of a paragraph.

🔴 This is what sealed sender is for, arriving by another door. Sealed sender removes from_user so an envelope cannot be attributed — from_user = 0 on all 14 live rows, so it is working. Length, timing and the per-device index together still describe a conversation: who received, when, how much, in what rhythm.

⚠️ The edge log holds a second copy. prune-edge-logs.sh already records that Traefik's access log carries RequestContentSize, which "leaks message length through the ciphertext". Padding fixes both, because both observe the same bytes.

Where the change has to live — not here

🔴 The relay cannot pad what it cannot read. Padding has to be applied by the sender before encryption, inside the sealed envelope, or the relay would be adding bytes to a blob it must not interpret. So this is:

  • flamenet-e2e — the framing, and a spec section
  • both apps — sender-side padding, receiver-side stripping
  • a compatibility story — an unpadded sender and a padded receiver have to interoperate in both directions across a staged rollout

It is not a relay change, despite being a relay-table finding.

What a ladder costs — measured, not asserted

Against the 14 real envelopes:

ladder buckets overhead
1 KiB steps 4 6.3%
×1.1 geometric 5 6.4%
×1.25 geometric 4 10.7%
×1.5 geometric 4 11.0%
4 KiB steps 4 24.4%
powers of two 3 55.5%

⚠️ Do not read that table as "pick 1 KiB steps". It is the wrong metric and it picks the wrong ladder.

Total overhead is dominated by the largest messages, and the observed sample is a narrow 6–17 KB band with no images in it at all. On a synthetic mix spanning receipts to a 2.4 MB photo, 1 KiB steps score better still — 1.0% — precisely because they barely pad the big messages. But a 2.4 MB image padded to the next kilobyte is known to within 0.04%, which is no hiding at all.

A geometric ladder gives constant relative resolution: at ×1.25, every message at every scale is known only to within 25%. A fixed step gives perfect resolution on exactly the messages whose size is most revealing.

So the metric to design against is not bytes added. It is how many plausible messages share a bucket at each scale, and that needs a real corpus rather than 14 rows from two test accounts.

Definition of done

  • A ladder chosen against bucket occupancy, not total overhead, with the corpus it was chosen against written down
  • Padding applied sender-side inside the sealed envelope; receiver strips it
  • Cross-version interop tested both directions — padded sender to unpadded receiver and back
  • /metadata says what is now hidden and what is still visible; ⚠️ it currently does not mention size at all
  • A test that fails if padding is skipped for any message kind — the fast path for small payloads is exactly where this would quietly stop applying

Honest limits

⚠️ Padding does not hide timing or count, and #18 found that arrival time, delivery order and the envelopes_inbox index on (to_device, id) are enough on their own to infer waking hours, time zone, and which two devices are in a conversation. Size is one of three signals; fixing it should not be described as fixing the set.

⚠️ 14 envelopes from two accounts is not a corpus. Every number above is a measurement of a sample too small to generalise from, and the first task in this ticket is getting a better one.

Related: #18 (metadata minimisation), #19 (identifying data), docs/METADATA-PASSES.md.

Split out of #18, which asked what an operator watching the `envelopes` table for a week could reconstruct. This is the part of the answer that is a change rather than a note. ## The finding `payload` is opaque ciphertext, which is what `/metadata` says. Its **length is not opaque**, and nothing hides it. Measured on production 2026-09-07: ``` select length(payload), count(*) from envelopes group by 1 order by 1; ``` **Fourteen envelopes, fourteen distinct lengths**, clustering by kind: | bytes | envelopes | |---|---| | ~6.3 K | 2 | | ~9.3–9.4 K | 6 | | ~13.9 K | 4 | | ~16.7 K | 2 | An operator reading the table sees each message's size to the byte, and sizes group by kind. A shape is a guess about content: a receipt is not the size of a photo, and a one-word reply is not the size of a paragraph. 🔴 **This is what sealed sender is for, arriving by another door.** Sealed sender removes `from_user` so an envelope cannot be attributed — `from_user = 0` on all 14 live rows, so it is working. Length, timing and the per-device index together still describe a conversation: who received, when, how much, in what rhythm. ⚠️ **The edge log holds a second copy.** `prune-edge-logs.sh` already records that Traefik's access log carries `RequestContentSize`, which "leaks message length through the ciphertext". Padding fixes both, because both observe the same bytes. ## Where the change has to live — not here 🔴 **The relay cannot pad what it cannot read.** Padding has to be applied by the sender before encryption, inside the sealed envelope, or the relay would be adding bytes to a blob it must not interpret. So this is: - `flamenet-e2e` — the framing, and a spec section - both apps — sender-side padding, receiver-side stripping - a compatibility story — an unpadded sender and a padded receiver have to interoperate in both directions across a staged rollout It is *not* a relay change, despite being a relay-table finding. ## What a ladder costs — measured, not asserted Against the 14 real envelopes: | ladder | buckets | overhead | |---|---|---| | 1 KiB steps | 4 | **6.3%** | | ×1.1 geometric | 5 | 6.4% | | ×1.25 geometric | 4 | 10.7% | | ×1.5 geometric | 4 | 11.0% | | 4 KiB steps | 4 | 24.4% | | powers of two | 3 | 55.5% | ⚠️ **Do not read that table as "pick 1 KiB steps".** It is the wrong metric and it picks the wrong ladder. Total overhead is dominated by the largest messages, and the observed sample is a narrow 6–17 KB band with no images in it at all. On a synthetic mix spanning receipts to a 2.4 MB photo, 1 KiB steps score *better* still — 1.0% — precisely because they barely pad the big messages. But a 2.4 MB image padded to the next kilobyte is known to within 0.04%, which is no hiding at all. **A geometric ladder gives constant *relative* resolution:** at ×1.25, every message at every scale is known only to within 25%. A fixed step gives perfect resolution on exactly the messages whose size is most revealing. So the metric to design against is not bytes added. It is **how many plausible messages share a bucket at each scale**, and that needs a real corpus rather than 14 rows from two test accounts. ## Definition of done - [ ] A ladder chosen against bucket occupancy, not total overhead, with the corpus it was chosen against written down - [ ] Padding applied sender-side inside the sealed envelope; receiver strips it - [ ] Cross-version interop tested both directions — padded sender to unpadded receiver and back - [ ] `/metadata` says what is now hidden and what is still visible; ⚠️ it currently does not mention size at all - [ ] A test that fails if padding is skipped for any message kind — the fast path for small payloads is exactly where this would quietly stop applying ## Honest limits ⚠️ **Padding does not hide timing or count**, and #18 found that arrival time, delivery order and the `envelopes_inbox` index on `(to_device, id)` are enough on their own to infer waking hours, time zone, and which two devices are in a conversation. Size is one of three signals; fixing it should not be described as fixing the set. ⚠️ **14 envelopes from two accounts is not a corpus.** Every number above is a measurement of a sample too small to generalise from, and the first task in this ticket is getting a better one. Related: #18 (metadata minimisation), #19 (identifying data), `docs/METADATA-PASSES.md`.
paulhitt commented 2026-09-07 13:32:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #18

mentioned in issue #18
paulhitt commented 2026-09-09 11:49:48 +00:00 (Migrated from gitlab.com)

First box is done; the padding half is not.

/metadata now carries the disclosure, in the "Arrival time and exact size"
row: that the length is the ciphertext itself and is not hidden to the byte,
that sizes group by kind (a read receipt is not the size of a photo), that
nothing has to be decrypted to see it, and that hiding it means padding on the
sending device because the server cannot pad what it must not read. It also
says plainly that this has not been done yet.

  • /metadata says what is visible
  • Padding, and a test that fails if it is skipped for any message kind

Remaining scope is a client change in both apps, not a relay change, so this
ticket is now about the apps.

First box is done; the padding half is not. `/metadata` now carries the disclosure, in the "Arrival time and **exact size**" row: that the length is the ciphertext itself and is not hidden to the byte, that sizes group by kind (a read receipt is not the size of a photo), that nothing has to be decrypted to see it, and that hiding it means padding on the sending device because the server cannot pad what it must not read. It also says plainly that this has not been done yet. - [x] `/metadata` says what is visible - [ ] Padding, and a test that fails if it is skipped for any message kind Remaining scope is a client change in both apps, not a relay change, so this ticket is now about the apps.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: extant2000/flamenet-relay#59