mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 23:05:20 +00:00
Discard deferred Noise ciphertext during panic
This commit is contained in:
@@ -184,6 +184,16 @@ final class BLENoisePacketHandler {
|
||||
drainDeferredCiphertextsIfReady(for: peerID)
|
||||
}
|
||||
|
||||
/// Synchronously discards ciphertext retained for a pre-panic Noise
|
||||
/// generation. The handler survives the service's identity replacement,
|
||||
/// so keeping this queue would replay old bytes after post-panic auth.
|
||||
func resetForPanic() {
|
||||
deferredLock.lock()
|
||||
deferredCiphertexts.removeAll(keepingCapacity: false)
|
||||
deferredCiphertextBytes = 0
|
||||
deferredLock.unlock()
|
||||
}
|
||||
|
||||
private func handleEncrypted(
|
||||
_ packet: BitchatPacket,
|
||||
from peerID: PeerID,
|
||||
|
||||
@@ -707,6 +707,7 @@ final class BLEService: NSObject {
|
||||
/// or advertising while the full panic transaction is incomplete.
|
||||
func suspendForPanicReset() {
|
||||
setPanicSuspended(true)
|
||||
noisePacketHandler.resetForPanic()
|
||||
gossipSyncManager?.stop()
|
||||
gossipSyncManager = nil
|
||||
// Stop the radio and drain CoreBluetooth's delegate queue first. A
|
||||
@@ -717,7 +718,11 @@ final class BLEService: NSObject {
|
||||
// Drain every receive/send submitted by callbacks that finished ahead
|
||||
// of the radio stop. Later callbacks observe the closed lifecycle, and
|
||||
// generation-bound handoffs that raced this barrier reject themselves.
|
||||
messageQueue.sync(flags: .barrier) {}
|
||||
// Clear the old identity's bounded early-ciphertext queue again after
|
||||
// those callbacks drain so none can repopulate it after the first wipe.
|
||||
messageQueue.sync(flags: .barrier) {
|
||||
noisePacketHandler.resetForPanic()
|
||||
}
|
||||
clearEmergencySessionState()
|
||||
}
|
||||
|
||||
@@ -736,6 +741,7 @@ final class BLEService: NSObject {
|
||||
gossipSyncManager?.stop()
|
||||
gossipSyncManager = nil
|
||||
messageQueue.sync(flags: .barrier) {
|
||||
noisePacketHandler.resetForPanic()
|
||||
pendingNoiseSessionQueues.removeAll()
|
||||
}
|
||||
|
||||
|
||||
@@ -521,6 +521,60 @@ struct BLENoisePacketHandlerTests {
|
||||
#expect(recorder.initiatedHandshakes.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func panicResetDiscardsDeferredCiphertextBeforeFutureAuthentication() {
|
||||
let recorder = Recorder()
|
||||
recorder.hasSession = true
|
||||
recorder.awaitingResponderHandshake = true
|
||||
recorder.decryptResult = .failure(
|
||||
CryptoKitError.authenticationFailure
|
||||
)
|
||||
let handler = makeHandler(recorder: recorder)
|
||||
let prePanicCiphertext = makeEncryptedPacket(
|
||||
recipientID: Data(hexString: localPeerID.id),
|
||||
payload: Data(
|
||||
count: NoiseSecurityConstants.maxMessageSize
|
||||
+ NoiseSecurityConstants.transportCiphertextOverhead
|
||||
)
|
||||
)
|
||||
|
||||
handler.handleEncrypted(prePanicCiphertext, from: remotePeerID)
|
||||
#expect(recorder.decryptCalls.count == 1)
|
||||
|
||||
handler.resetForPanic()
|
||||
|
||||
// Three maximum-sized packets fit only when reset also zeroed the
|
||||
// global byte accounting. They model ciphertext received under the
|
||||
// replacement identity before that responder handshake completes.
|
||||
for index in 0..<3 {
|
||||
handler.handleEncrypted(
|
||||
makeEncryptedPacket(
|
||||
recipientID: Data(hexString: localPeerID.id),
|
||||
timestamp: UInt64(901_000 + index),
|
||||
payload: Data(
|
||||
count: NoiseSecurityConstants.maxMessageSize
|
||||
+ NoiseSecurityConstants.transportCiphertextOverhead
|
||||
)
|
||||
),
|
||||
from: remotePeerID
|
||||
)
|
||||
}
|
||||
#expect(recorder.decryptCalls.count == 4)
|
||||
|
||||
recorder.awaitingResponderHandshake = false
|
||||
recorder.decryptResult = .success(
|
||||
Data([NoisePayloadType.privateMessage.rawValue, 0xCA, 0xFE])
|
||||
)
|
||||
handler.handleSessionAuthenticated(remotePeerID)
|
||||
|
||||
// Only the three post-reset packets replay; the pre-panic packet does
|
||||
// not survive into the replacement session.
|
||||
#expect(recorder.decryptCalls.count == 7)
|
||||
#expect(recorder.deliveries.count == 3)
|
||||
#expect(recorder.clearedSessions.isEmpty)
|
||||
#expect(recorder.initiatedHandshakes.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func ciphertextQueuedAheadOfEstablishmentCallbackDoesNotConsumeNonce()
|
||||
throws {
|
||||
|
||||
Reference in New Issue
Block a user