From 4d2ade74dd81f0b06673075722456c30975a2d8b Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 26 Jul 2026 12:47:38 +0200 Subject: [PATCH] Keep timeout-restore queues parked through the same-generation ready path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deferral-buffer rework routes quarantine restores through a same-generation ready transition that no longer rebuilds capability state — but it drained both outbound queues unconditionally there, bypassing the pending-convergence gate. Honor the restore reason in that branch too: retrying deferred inbound ciphertext stays (it is receive-side and safe under restored keys), while queue drains wait for the convergence retry's establishment exactly as on the new-generation path. Terminal restores keep draining immediately. Also pre-drain the establishment's serialized forced announce in the terminal-restore test so the tap only observes restore-driven traffic. Co-Authored-By: Claude Opus 4.8 --- bitchat/Services/BLE/BLEService.swift | 17 ++++++++++++++++- bitchatTests/BLEServiceCoreTests.swift | 4 ++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/bitchat/Services/BLE/BLEService.swift b/bitchat/Services/BLE/BLEService.swift index 32b758ed..33a5b7c5 100644 --- a/bitchat/Services/BLE/BLEService.swift +++ b/bitchat/Services/BLE/BLEService.swift @@ -4892,8 +4892,23 @@ extension BLEService { // A quarantined transport restored the same cryptographic // generation. Its capability proof and announce state never // became stale; only work queued while outbound keys were paused - // needs one idempotent ready transition. + // needs one idempotent ready transition. Retrying the bounded + // early-ciphertext queue is receive-side and therefore always + // safe under the restored keys. noisePacketHandler.handleSessionAuthenticated(normalizedPeerID) + #if DEBUG + _test_onPrivateMediaSessionReconciled?(normalizedPeerID) + #endif + if deferOutboundUntilConvergence { + // Timeout-restore: the counterpart may have completed the + // replacement handshake and discarded these keys, so + // encrypting the parked queues here would lose them silently. + // The restore's mandatory convergence retry — or any later + // handshake the reconnect policy initiates — re-enters this + // transition with a fresh generation and drains them under + // keys both sides hold. + return + } sendPendingMessagesAfterHandshake(for: normalizedPeerID) sendPendingNoisePayloadsAfterHandshake(for: normalizedPeerID) return diff --git a/bitchatTests/BLEServiceCoreTests.swift b/bitchatTests/BLEServiceCoreTests.swift index 1c958264..aa0a6069 100644 --- a/bitchatTests/BLEServiceCoreTests.swift +++ b/bitchatTests/BLEServiceCoreTests.swift @@ -666,6 +666,10 @@ struct BLEServiceCoreTests { ) await ble._test_drainNoiseMessagePipeline() #expect(ble.canDeliverSecurely(to: alicePeerID)) + // The establishment transition enqueues its forced announce as a + // separate serialized phase; drain again so it lands before the tap + // and cannot masquerade as restore-driven announce traffic below. + await ble._test_drainNoiseMessagePipeline() let outbound = OutboundPacketTap() ble._test_onOutboundPacket = outbound.record