mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 20:05:19 +00:00
BLE privacy: derive peerID from Noise fingerprint; remove BLE Local Name from advertising; verify announces against key-derived ID; auto-initiate Noise handshake when encrypted message arrives without session; drop rotating alias option. (#447)
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
import Combine
|
import Combine
|
||||||
|
import CryptoKit
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
@@ -75,6 +76,9 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
var myNickname: String = "Anonymous"
|
var myNickname: String = "Anonymous"
|
||||||
private let noiseService = NoiseEncryptionService()
|
private let noiseService = NoiseEncryptionService()
|
||||||
|
|
||||||
|
// MARK: - Advertising Privacy
|
||||||
|
// No Local Name by default for maximum privacy. No rotating alias.
|
||||||
|
|
||||||
// MARK: - Queues
|
// MARK: - Queues
|
||||||
|
|
||||||
private let messageQueue = DispatchQueue(label: "mesh.message", attributes: .concurrent)
|
private let messageQueue = DispatchQueue(label: "mesh.message", attributes: .concurrent)
|
||||||
@@ -137,8 +141,9 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// Generate ephemeral peer ID (8 random bytes as hex)
|
// Derive stable peer ID from Noise static public key fingerprint (first 8 bytes → 16 hex chars)
|
||||||
self.myPeerID = (0..<8).map { _ in String(format: "%02x", UInt8.random(in: 0...255)) }.joined()
|
let fingerprint = noiseService.getIdentityFingerprint() // 64 hex chars
|
||||||
|
self.myPeerID = String(fingerprint.prefix(16))
|
||||||
|
|
||||||
// Set queue key for identification
|
// Set queue key for identification
|
||||||
messageQueue.setSpecific(key: messageQueueKey, value: ())
|
messageQueue.setSpecific(key: messageQueueKey, value: ())
|
||||||
@@ -200,6 +205,8 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
sendAnnounce(forceSend: true)
|
sendAnnounce(forceSend: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No advertising policy to set; we never include Local Name in adverts.
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
maintenanceTimer?.invalidate()
|
maintenanceTimer?.invalidate()
|
||||||
centralManager?.stopScan()
|
centralManager?.stopScan()
|
||||||
@@ -219,6 +226,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
centralManager?.stopScan()
|
centralManager?.stopScan()
|
||||||
startScanning()
|
startScanning()
|
||||||
}
|
}
|
||||||
|
// No Local Name; nothing to refresh for advertising policy
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appDidEnterBackground() {
|
@objc private func appDidEnterBackground() {
|
||||||
@@ -228,6 +236,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
centralManager?.stopScan()
|
centralManager?.stopScan()
|
||||||
startScanning()
|
startScanning()
|
||||||
}
|
}
|
||||||
|
// No Local Name; nothing to refresh for advertising policy
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -972,6 +981,16 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that the sender's derived ID from the announced public key matches the packet senderID
|
||||||
|
// This helps detect relayed or spoofed announces. Only warn in release; assert in debug.
|
||||||
|
let derivedFromKey = Self.derivePeerID(fromPublicKey: announcement.publicKey)
|
||||||
|
if derivedFromKey != peerID {
|
||||||
|
SecureLogger.log("⚠️ Announce sender mismatch: derived \(derivedFromKey.prefix(8))… vs packet \(peerID.prefix(8))…", category: SecureLogger.security, level: .warning)
|
||||||
|
#if DEBUG
|
||||||
|
assertionFailure("Announce senderID does not match key-derived ID")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// Don't add ourselves as a peer
|
// Don't add ourselves as a peer
|
||||||
if peerID == myPeerID {
|
if peerID == myPeerID {
|
||||||
return
|
return
|
||||||
@@ -1254,6 +1273,14 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
default:
|
default:
|
||||||
SecureLogger.log("⚠️ Unknown noise payload type: \(payloadType)", category: SecureLogger.noise, level: .warning)
|
SecureLogger.log("⚠️ Unknown noise payload type: \(payloadType)", category: SecureLogger.noise, level: .warning)
|
||||||
}
|
}
|
||||||
|
} catch NoiseEncryptionError.sessionNotEstablished {
|
||||||
|
// We received an encrypted message before establishing a session with this peer.
|
||||||
|
// Trigger a handshake so future messages can be decrypted.
|
||||||
|
SecureLogger.log("🔑 Encrypted message from \(peerID) without session; initiating handshake",
|
||||||
|
category: SecureLogger.noise, level: .info)
|
||||||
|
if !noiseService.hasSession(with: peerID) {
|
||||||
|
initiateNoiseHandshake(with: peerID)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("❌ Failed to decrypt message from \(peerID): \(error)",
|
SecureLogger.log("❌ Failed to decrypt message from \(peerID): \(error)",
|
||||||
category: SecureLogger.noise, level: .error)
|
category: SecureLogger.noise, level: .error)
|
||||||
@@ -1395,10 +1422,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
if peers.isEmpty {
|
if peers.isEmpty {
|
||||||
// Ensure we're advertising as peripheral
|
// Ensure we're advertising as peripheral
|
||||||
if let pm = peripheralManager, pm.state == .poweredOn && !pm.isAdvertising {
|
if let pm = peripheralManager, pm.state == .poweredOn && !pm.isAdvertising {
|
||||||
pm.startAdvertising([
|
pm.startAdvertising(buildAdvertisementData())
|
||||||
CBAdvertisementDataServiceUUIDsKey: [SimplifiedBluetoothService.serviceUUID],
|
|
||||||
CBAdvertisementDataLocalNameKey: myPeerID
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1412,6 +1436,8 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
performCleanup()
|
performCleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No rotating alias: nothing to refresh
|
||||||
|
|
||||||
// Reset counter to prevent overflow (every 60 seconds)
|
// Reset counter to prevent overflow (every 60 seconds)
|
||||||
if maintenanceCounter >= 6 {
|
if maintenanceCounter >= 6 {
|
||||||
maintenanceCounter = 0
|
maintenanceCounter = 0
|
||||||
@@ -1871,13 +1897,11 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
SecureLogger.log("✅ Service added successfully, starting advertising", category: SecureLogger.session, level: .info)
|
SecureLogger.log("✅ Service added successfully, starting advertising", category: SecureLogger.session, level: .info)
|
||||||
|
|
||||||
// Now start advertising after service is confirmed added
|
// Start advertising after service is confirmed added
|
||||||
peripheral.startAdvertising([
|
let adData = buildAdvertisementData()
|
||||||
CBAdvertisementDataServiceUUIDsKey: [SimplifiedBluetoothService.serviceUUID],
|
peripheral.startAdvertising(adData)
|
||||||
CBAdvertisementDataLocalNameKey: myPeerID
|
|
||||||
])
|
|
||||||
|
|
||||||
SecureLogger.log("📡 Started advertising as peripheral with ID: \(myPeerID)", category: SecureLogger.session, level: .info)
|
SecureLogger.log("📡 Started advertising (LocalName: \((adData[CBAdvertisementDataLocalNameKey] as? String) != nil ? "on" : "off"), ID: \(myPeerID.prefix(8))…)", category: SecureLogger.session, level: .info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
||||||
@@ -1895,10 +1919,7 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
|
|||||||
// Ensure we're still advertising for other devices to find us
|
// Ensure we're still advertising for other devices to find us
|
||||||
if peripheral.isAdvertising == false {
|
if peripheral.isAdvertising == false {
|
||||||
SecureLogger.log("📡 Restarting advertising after central unsubscribed", category: SecureLogger.session, level: .info)
|
SecureLogger.log("📡 Restarting advertising after central unsubscribed", category: SecureLogger.session, level: .info)
|
||||||
peripheral.startAdvertising([
|
peripheral.startAdvertising(buildAdvertisementData())
|
||||||
CBAdvertisementDataServiceUUIDsKey: [SimplifiedBluetoothService.serviceUUID],
|
|
||||||
CBAdvertisementDataLocalNameKey: myPeerID
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find and disconnect the peer associated with this central
|
// Find and disconnect the peer associated with this central
|
||||||
@@ -2058,6 +2079,26 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Advertising Builders & Alias Rotation
|
||||||
|
|
||||||
|
extension SimplifiedBluetoothService {
|
||||||
|
static func derivePeerID(fromPublicKey publicKey: Data) -> String {
|
||||||
|
let digest = SHA256.hash(data: publicKey)
|
||||||
|
let hex = digest.map { String(format: "%02x", $0) }.joined()
|
||||||
|
return String(hex.prefix(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildAdvertisementData() -> [String: Any] {
|
||||||
|
var data: [String: Any] = [
|
||||||
|
CBAdvertisementDataServiceUUIDsKey: [SimplifiedBluetoothService.serviceUUID]
|
||||||
|
]
|
||||||
|
// No Local Name for privacy
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// No alias rotation or advertising restarts required.
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Message Deduplicator
|
// MARK: - Message Deduplicator
|
||||||
|
|
||||||
/// Efficient message deduplication with time-based cleanup
|
/// Efficient message deduplication with time-based cleanup
|
||||||
|
|||||||
Reference in New Issue
Block a user