From 2371d5d5ddb3ed881b83958c657629345d9c8e66 Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Mon, 7 Sep 2026 20:32:06 -0400 Subject: [PATCH] /metadata: the size it keeps is exact, not approximate (relay#59) The page called the stored ciphertext length an "approximate size". It is not approximate. The length is exact, and lengths group by kind -- a read receipt is not the size of a photo -- so a byte count is a weak but real guess about what an envelope carries, with nothing decrypted. A transparency page that undersells what it keeps is worse than one that keeps more and says so plainly, so the row now states what the length gives away and why it is still there: hiding it means padding on the sending device before encryption, which is a change to the apps, not to the server -- the server cannot pad what it must not read. This is the half of relay#59 that is a note. The padding itself spans flamenet-e2e and both apps and needs a two-way compatibility story across a staged rollout, so it stays open. Co-Authored-By: Claude Opus 5 --- Resources/Views/metadata.leaf | 2 +- Tests/FlamenetRelayTests/PublicPagesTests.swift | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Resources/Views/metadata.leaf b/Resources/Views/metadata.leaf index 0d6091e..e736551 100644 --- a/Resources/Views/metadata.leaf +++ b/Resources/Views/metadata.leaf @@ -29,7 +29,7 @@ KeptWhy it has to beGone after Recipient account and deviceAn envelope cannot be delivered without an address#if(retention.deleteOnAck):the moment your app confirms receipt#else:#(retention.deliveredDays) day(s) once delivered#endif - Arrival time and approximate sizeOrdering, and the size is the ciphertext itself#if(retention.deleteOnAck):the moment your app confirms receipt#else:#(retention.deliveredDays) day(s) once delivered#endif + Arrival time and exact sizeOrdering, and the size is the ciphertext itself — so the length of a message is not hidden, to the byte. Sizes group by kind: a read receipt is not the size of a photo, and a one-word reply is not the size of a paragraph. Nothing has to be decrypted to see that, and it is a weak but real guess about what an envelope carries. Hiding it means padding messages on the sending device before they are encrypted — a change to the apps rather than to this server, because the server cannot pad what it must not read. It has not been made yet.#if(retention.deleteOnAck):the moment your app confirms receipt#else:#(retention.deliveredDays) day(s) once delivered#endif Sender account and deviceIdentified sends only — a sealed send stores neither#if(retention.deleteOnAck):the moment your app confirms receipt#else:#(retention.deliveredDays) day(s) once delivered#endif The ciphertextIt is the message; the server holds no key that opens it#(retention.undeliveredDays) days if never collected Attachment blobs (photos, voice notes, stories — all ciphertext)Devices fetch them separately from the envelope that references them#(retention.attachmentDays) days diff --git a/Tests/FlamenetRelayTests/PublicPagesTests.swift b/Tests/FlamenetRelayTests/PublicPagesTests.swift index 8335083..0fbdb41 100644 --- a/Tests/FlamenetRelayTests/PublicPagesTests.swift +++ b/Tests/FlamenetRelayTests/PublicPagesTests.swift @@ -213,6 +213,21 @@ final class PublicPagesTests: XCTestCase { XCTAssertEqual(offenders, [], "\n" + offenders.joined(separator: "\n")) } + /// ⚠️ **The transparency page has to be accurate in the unflattering + /// direction too.** `/metadata` called what it stores the "approximate + /// size" of a message. It is not approximate: the ciphertext length is + /// exact, and lengths group by kind, so a byte count is a weak guess about + /// what an envelope carries (relay#59). A page that undersells what it + /// keeps is worse than one that keeps more and says so. + func testMetadataDoesNotUndersellWhatItKeeps() throws { + let page = try String(contentsOf: views.appendingPathComponent("metadata.leaf"), + encoding: .utf8) + XCTAssertFalse(page.localizedCaseInsensitiveContains("approximate size"), + "the stored ciphertext length is exact, not approximate") + XCTAssertTrue(page.localizedCaseInsensitiveContains("exact size"), + "the page no longer states that the size it keeps is exact") + } + func testTheSafetyPageCarriesAReachableContact() throws { let page = try String(contentsOf: views.appendingPathComponent("safety.leaf"), encoding: .utf8) -- 2.54.0