Persist authenticated private-media delivery receipts (#1466)

Adds a durable receiver-side ledger (BLEPrivateMediaReceiptStore) mapping a deterministic private-media message ID — hash-bound to sender, recipient, and entropy-bearing filename — to the stored file before UI delivery and before the Noise-encrypted delivery ACK, so sender retries and relaunches cannot create duplicate bubbles or files. Receipts are unforgeable without the Noise session; capability bit 9 rides the existing announce bitfield (no wire-format change; rolling upgrade safe both directions).

Includes review fixes: corrupt receipt records are quarantined per-record (bytes preserved at <id>.json.corrupt, only that ID fail-closed — previously one bad record silently disabled ALL inbound private media forever); the panic reset now reaches BLEService's own store instance via completePanicReset with a production-wiring test; and both content.delivery.reason.* strings ship with full 30-locale coverage.
This commit is contained in:
jack
2026-07-26 14:00:21 +02:00
committed by GitHub
parent a9ceddab21
commit fb451bc6d0
22 changed files with 2677 additions and 62 deletions
@@ -682,13 +682,18 @@ struct PrivateMediaEndToEndTests {
#expect(!identity.hasObservedPrivateMediaCapability(
fingerprint: impostorKey.sha256Fingerprint()
))
#expect(identity.hasObservedPrivateMediaCapability(
fingerprint: bob.noiseStaticPublicKeyData().sha256Fingerprint()
))
alice._test_seedConnectedPeer(
bob.myPeerID,
nickname: "Bob",
capabilities: [],
noisePublicKey: impostorKey
)
#expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .legacyRequiresConsent)
// The exact live Noise identity remains authoritative over a later
// impostor registry rewrite.
#expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .encrypted)
}
@Test
@@ -876,7 +881,7 @@ struct PrivateMediaEndToEndTests {
+ marker
+ Data(repeating: 0x4A, count: 6 * 1024)
try await assertPrivateMediaRoundTrip(
fileName: "private.jpg",
fileName: "img_20260725_120000_11111111-1111-1111-1111-111111111111.jpg",
mimeType: "image/jpeg",
content: content,
marker: marker,
@@ -1147,6 +1152,17 @@ struct PrivateMediaEndToEndTests {
#expect(message.isPrivate)
#expect(message.senderPeerID == alice.myPeerID)
#expect(message.content.hasPrefix(expectedMessagePrefix))
if let stableMessageID = PrivateMediaMessageIdentity.stableID(
for: file,
senderPeerID: alice.myPeerID,
recipientPeerID: bob.myPeerID
) {
#expect(message.id == stableMessageID)
} else {
// Generic/legacy filenames retain random per-arrival IDs so two
// unrelated "photo.jpg" transfers are never deduplicated.
#expect(!message.id.hasPrefix("media-"))
}
let stored = recursivelyStoredFiles(under: bobRoot)
#expect(stored.count == 1)
@@ -1289,6 +1305,8 @@ struct PrivateMediaEndToEndTests {
return enumerator.compactMap { item in
guard let url = item as? URL,
!url.pathComponents.contains(".private-media-receipts"),
url.lastPathComponent != ".private-media-receipts.json",
(try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true else {
return nil
}