Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 40 additions & 18 deletions bitchat/Noise/NoiseSessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,37 @@ final class NoiseSessionManager {
private let localStaticKey: Curve25519.KeyAgreement.PrivateKey
private let keychain: KeychainManagerProtocol
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)

private let managerQueueKey = DispatchSpecificKey<Void>()

// Callbacks
var onSessionEstablished: ((PeerID, Curve25519.KeyAgreement.PublicKey) -> Void)?
var onSessionFailed: ((PeerID, Error) -> Void)?

init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) {
self.localStaticKey = localStaticKey
self.keychain = keychain
managerQueue.setSpecific(key: managerQueueKey, value: ())
}

/// Check if currently executing on the manager queue (for debugging re-entrancy issues)
private var isOnManagerQueue: Bool {
DispatchQueue.getSpecific(key: managerQueueKey) != nil
}

// MARK: - Session Management

func getSession(for peerID: PeerID) -> NoiseSession? {
// Re-entrancy check: if already on managerQueue, access directly to avoid deadlock
if isOnManagerQueue {
return sessions[peerID]
}
return managerQueue.sync {
return sessions[peerID]
}
}

func removeSession(for peerID: PeerID) {
assert(!isOnManagerQueue, "removeSession called from managerQueue - potential deadlock")
managerQueue.sync(flags: .barrier) {
if let session = sessions.removeValue(forKey: peerID) {
session.reset() // Clear sensitive data before removing
Expand All @@ -42,6 +54,7 @@ final class NoiseSessionManager {
}

func removeAllSessions() {
assert(!isOnManagerQueue, "removeAllSessions called from managerQueue - potential deadlock")
managerQueue.sync(flags: .barrier) {
for (_, session) in sessions {
session.reset()
Expand All @@ -51,8 +64,9 @@ final class NoiseSessionManager {
}

// MARK: - Handshake Helpers

func initiateHandshake(with peerID: PeerID) throws -> Data {
assert(!isOnManagerQueue, "initiateHandshake called from managerQueue - potential deadlock")
return try managerQueue.sync(flags: .barrier) {
// Check if we already have an established session
if let existingSession = sessions[peerID], existingSession.isEstablished() {
Expand Down Expand Up @@ -87,6 +101,7 @@ final class NoiseSessionManager {
}

func handleIncomingHandshake(from peerID: PeerID, message: Data) throws -> Data? {
assert(!isOnManagerQueue, "handleIncomingHandshake called from managerQueue - potential deadlock")
// Process everything within the synchronized block to prevent race conditions
return try managerQueue.sync(flags: .barrier) {
var shouldCreateNew = false
Expand Down Expand Up @@ -184,27 +199,34 @@ final class NoiseSessionManager {
}

// MARK: - Session Rekeying

func getSessionsNeedingRekey() -> [(peerID: PeerID, needsRekey: Bool)] {
// Re-entrancy check: if already on managerQueue, access directly to avoid deadlock
if isOnManagerQueue {
return computeSessionsNeedingRekey()
}
return managerQueue.sync {
var needingRekey: [(peerID: PeerID, needsRekey: Bool)] = []

for (peerID, session) in sessions {
if let secureSession = session as? SecureNoiseSession,
secureSession.isEstablished(),
secureSession.needsRenegotiation() {
needingRekey.append((peerID: peerID, needsRekey: true))
}
return computeSessionsNeedingRekey()
}
}

private func computeSessionsNeedingRekey() -> [(peerID: PeerID, needsRekey: Bool)] {
var needingRekey: [(peerID: PeerID, needsRekey: Bool)] = []
for (peerID, session) in sessions {
if let secureSession = session as? SecureNoiseSession,
secureSession.isEstablished(),
secureSession.needsRenegotiation() {
needingRekey.append((peerID: peerID, needsRekey: true))
}

return needingRekey
}
return needingRekey
}

func initiateRekey(for peerID: PeerID) throws {
assert(!isOnManagerQueue, "initiateRekey called from managerQueue - potential deadlock")
// Remove old session
removeSession(for: peerID)

// Initiate new handshake
_ = try initiateHandshake(with: peerID)
}
Expand Down
30 changes: 19 additions & 11 deletions bitchat/Services/BLE/BLEService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2713,23 +2713,31 @@ extension BLEService {

#if os(iOS)
@objc private func appDidBecomeActive() {
isAppActive = true
// Restart scanning with allow duplicates when app becomes active
if centralManager?.state == .poweredOn {
centralManager?.stopScan()
startScanning()
// Dispatch to bleQueue for thread-safe access to isAppActive and BLE operations
bleQueue.async { [weak self] in
guard let self = self else { return }
self.isAppActive = true
// Restart scanning with allow duplicates when app becomes active
if self.centralManager?.state == .poweredOn {
self.centralManager?.stopScan()
self.startScanning()
}
}
logBluetoothStatus("became-active")
scheduleBluetoothStatusSample(after: 5.0, context: "active-5s")
// No Local Name; nothing to refresh for advertising policy
}

@objc private func appDidEnterBackground() {
isAppActive = false
// Restart scanning without allow duplicates in background
if centralManager?.state == .poweredOn {
centralManager?.stopScan()
startScanning()
// Dispatch to bleQueue for thread-safe access to isAppActive and BLE operations
bleQueue.async { [weak self] in
guard let self = self else { return }
self.isAppActive = false
// Restart scanning without allow duplicates in background
if self.centralManager?.state == .poweredOn {
self.centralManager?.stopScan()
self.startScanning()
}
}
logBluetoothStatus("entered-background")
scheduleBluetoothStatusSample(after: 15.0, context: "background-15s")
Expand Down
26 changes: 13 additions & 13 deletions bitchatTests/Fragmentation/FragmentationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ struct FragmentationTests {

// Inject fragments spaced out to avoid concurrent mutation inside BLEService
for (i, fragment) in shuffled.enumerated() {
let delay = 5 * Double(i) * 0.001
let delay = 10 * Double(i) * 0.001
Task {
try await sleep(delay)
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
}
}
// Allow async processing
try await sleep(0.5)

// Allow async processing (longer timeout for CI environments)
try await sleep(1.5)

#expect(capture.publicMessages.count == 1)
#expect(capture.publicMessages.first?.content.count == 3_000)
}
Expand All @@ -79,15 +79,15 @@ struct FragmentationTests {
}

for (i, fragment) in frags.enumerated() {
let delay = 5 * Double(i) * 0.001
let delay = 10 * Double(i) * 0.001
Task {
try await sleep(delay)
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
}
}
// Allow async processing
try await sleep(0.5)

// Allow async processing (longer timeout for CI environments)
try await sleep(1.5)

#expect(capture.publicMessages.count == 1)
#expect(capture.publicMessages.first?.content.count == 2048)
Expand Down Expand Up @@ -180,15 +180,15 @@ struct FragmentationTests {
}

for (i, fragment) in corrupted.enumerated() {
let delay = 5 * Double(i) * 0.001
let delay = 10 * Double(i) * 0.001
Task {
try await sleep(delay)
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
}
}
// Allow async processing
try await sleep(0.5)

// Allow async processing (longer timeout for CI environments)
try await sleep(1.5)

// Should not deliver since one fragment is invalid and reassembly can't complete
#expect(capture.publicMessages.isEmpty)
Expand Down