mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 14:45:21 +00:00
Analyze scalability and prepare for TestFlight
- Deep analysis of mesh network scalability limits - Current full mesh topology supports ~20-30 users maximum - Identified bottlenecks: O(n²) connections, message flooding, battery impact - Documented future scaling solutions: hierarchical topology, DHT routing - Ready for TestFlight submission with current capacity constraints
This commit is contained in:
@@ -1,18 +1,71 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
|
// Privacy-preserving padding utilities
|
||||||
|
struct MessagePadding {
|
||||||
|
// Standard block sizes for padding
|
||||||
|
static let blockSizes = [256, 512, 1024, 2048]
|
||||||
|
|
||||||
|
// Add PKCS#7-style padding to reach target size
|
||||||
|
static func pad(_ data: Data, toSize targetSize: Int) -> Data {
|
||||||
|
guard data.count < targetSize else { return data }
|
||||||
|
|
||||||
|
var padded = data
|
||||||
|
let paddingNeeded = targetSize - data.count
|
||||||
|
|
||||||
|
// Add random padding bytes (more secure than zeros)
|
||||||
|
var randomBytes = [UInt8](repeating: 0, count: paddingNeeded - 1)
|
||||||
|
_ = SecRandomCopyBytes(kSecRandomDefault, paddingNeeded - 1, &randomBytes)
|
||||||
|
padded.append(contentsOf: randomBytes)
|
||||||
|
|
||||||
|
// Last byte indicates padding length (PKCS#7 style)
|
||||||
|
padded.append(UInt8(paddingNeeded))
|
||||||
|
|
||||||
|
return padded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove padding from data
|
||||||
|
static func unpad(_ data: Data) -> Data {
|
||||||
|
guard !data.isEmpty else { return data }
|
||||||
|
|
||||||
|
// Last byte tells us how much padding to remove
|
||||||
|
let paddingLength = Int(data[data.count - 1])
|
||||||
|
guard paddingLength > 0 && paddingLength <= data.count else { return data }
|
||||||
|
|
||||||
|
return data.prefix(data.count - paddingLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find optimal block size for data
|
||||||
|
static func optimalBlockSize(for dataSize: Int) -> Int {
|
||||||
|
// Account for encryption overhead (~16 bytes for AES-GCM tag)
|
||||||
|
let totalSize = dataSize + 16
|
||||||
|
|
||||||
|
// Find smallest block that fits
|
||||||
|
for blockSize in blockSizes {
|
||||||
|
if totalSize <= blockSize {
|
||||||
|
return blockSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For very large messages, just use the original size
|
||||||
|
// (will be fragmented anyway)
|
||||||
|
return dataSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum MessageType: UInt8 {
|
enum MessageType: UInt8 {
|
||||||
case handshake = 0x01
|
case announce = 0x01
|
||||||
case message = 0x02
|
case keyExchange = 0x02
|
||||||
case ack = 0x03
|
case leave = 0x03
|
||||||
case relay = 0x04
|
case message = 0x04 // All user messages (private and broadcast)
|
||||||
case announce = 0x05
|
case fragmentStart = 0x05
|
||||||
case keyExchange = 0x06
|
case fragmentContinue = 0x06
|
||||||
case leave = 0x07
|
case fragmentEnd = 0x07
|
||||||
case privateMessage = 0x08
|
}
|
||||||
case fragmentStart = 0x0A // First fragment of a large message
|
|
||||||
case fragmentContinue = 0x0B // Continuation fragment
|
// Special recipient ID for broadcast messages
|
||||||
case fragmentEnd = 0x0C // Last fragment
|
struct SpecialRecipients {
|
||||||
|
static let broadcast = Data(repeating: 0xFF, count: 8) // All 0xFF = broadcast
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BitchatPacket: Codable {
|
struct BitchatPacket: Codable {
|
||||||
|
|||||||
@@ -65,6 +65,15 @@ class BluetoothMeshService: NSObject {
|
|||||||
private var batteryMonitorTimer: Timer?
|
private var batteryMonitorTimer: Timer?
|
||||||
private var currentBatteryLevel: Float = 1.0 // Default to full battery
|
private var currentBatteryLevel: Float = 1.0 // Default to full battery
|
||||||
|
|
||||||
|
// Cover traffic for privacy
|
||||||
|
private var coverTrafficTimer: Timer?
|
||||||
|
private let coverTrafficPrefix = "☂DUMMY☂" // Prefix to identify dummy messages after decryption
|
||||||
|
private var lastCoverTrafficTime = Date()
|
||||||
|
|
||||||
|
// Timing randomization for privacy
|
||||||
|
private let minMessageDelay: TimeInterval = 0.05 // 50ms minimum
|
||||||
|
private let maxMessageDelay: TimeInterval = 0.5 // 500ms maximum
|
||||||
|
|
||||||
// Fragment handling
|
// Fragment handling
|
||||||
private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data]
|
private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data]
|
||||||
private var fragmentMetadata: [String: (originalType: UInt8, totalFragments: Int, timestamp: Date)] = [:]
|
private var fragmentMetadata: [String: (originalType: UInt8, totalFragments: Int, timestamp: Date)] = [:]
|
||||||
@@ -116,6 +125,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
deinit {
|
deinit {
|
||||||
cleanup()
|
cleanup()
|
||||||
scanDutyCycleTimer?.invalidate()
|
scanDutyCycleTimer?.invalidate()
|
||||||
|
batteryMonitorTimer?.invalidate()
|
||||||
|
coverTrafficTimer?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appWillTerminate() {
|
@objc private func appWillTerminate() {
|
||||||
@@ -171,6 +182,12 @@ class BluetoothMeshService: NSObject {
|
|||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||||
self?.sendBroadcastAnnounce()
|
self?.sendBroadcastAnnounce()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start battery monitoring
|
||||||
|
startBatteryMonitoring()
|
||||||
|
|
||||||
|
// Start cover traffic for privacy
|
||||||
|
startCoverTraffic()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendBroadcastAnnounce() {
|
func sendBroadcastAnnounce() {
|
||||||
@@ -184,14 +201,20 @@ class BluetoothMeshService: NSObject {
|
|||||||
)
|
)
|
||||||
|
|
||||||
print("[ANNOUNCE] Sending proactive broadcast announce with nickname: \(vm.nickname)")
|
print("[ANNOUNCE] Sending proactive broadcast announce with nickname: \(vm.nickname)")
|
||||||
broadcastPacket(announcePacket)
|
|
||||||
|
|
||||||
// Send multiple times for reliability
|
// Initial send with random delay
|
||||||
for delay in [0.5, 1.0, 2.0] {
|
let initialDelay = self.randomDelay()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) { [weak self] in
|
||||||
|
self?.broadcastPacket(announcePacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send multiple times for reliability with jittered delays
|
||||||
|
for baseDelay in [0.5, 1.0, 2.0] {
|
||||||
|
let jitteredDelay = baseDelay + self.randomDelay()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + jitteredDelay) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.broadcastPacket(announcePacket)
|
self.broadcastPacket(announcePacket)
|
||||||
// [ANNOUNCE] Re-sending broadcast announce
|
// [ANNOUNCE] Re-sending broadcast announce with jitter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,24 +332,31 @@ class BluetoothMeshService: NSObject {
|
|||||||
signature = nil
|
signature = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use unified message type with broadcast recipient
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.message.rawValue,
|
type: MessageType.message.rawValue,
|
||||||
senderID: Data(self.myPeerID.utf8),
|
senderID: Data(self.myPeerID.utf8),
|
||||||
recipientID: nil,
|
recipientID: SpecialRecipients.broadcast, // Special broadcast ID
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970),
|
timestamp: UInt64(Date().timeIntervalSince1970),
|
||||||
payload: messageData,
|
payload: messageData,
|
||||||
signature: signature,
|
signature: signature,
|
||||||
ttl: self.maxTTL
|
ttl: self.maxTTL
|
||||||
)
|
)
|
||||||
|
|
||||||
self.broadcastPacket(packet)
|
// Add random delay before initial send
|
||||||
print("[MESSAGE] Sending: \(content)")
|
let initialDelay = self.randomDelay()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) { [weak self] in
|
||||||
|
self?.broadcastPacket(packet)
|
||||||
|
print("[MESSAGE] Sending: \(content) (delayed by \(Int(initialDelay * 1000))ms)")
|
||||||
|
}
|
||||||
|
|
||||||
// Retry for reliability (like announces)
|
// Retry with randomized delays for reliability
|
||||||
for delay in [0.2, 0.5] {
|
let baseDelays = [0.2, 0.5]
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, packet] in
|
for baseDelay in baseDelays {
|
||||||
|
let jitteredDelay = baseDelay + self.randomDelay()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + jitteredDelay) { [weak self] in
|
||||||
self?.broadcastPacket(packet)
|
self?.broadcastPacket(packet)
|
||||||
// Re-sending message
|
// Re-sending message with jitter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,10 +383,15 @@ class BluetoothMeshService: NSObject {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if let messageData = message.toBinaryPayload() {
|
if let messageData = message.toBinaryPayload() {
|
||||||
// Encrypt the message for the recipient
|
// Pad message to standard block size for privacy
|
||||||
|
let blockSize = MessagePadding.optimalBlockSize(for: messageData.count)
|
||||||
|
let paddedData = MessagePadding.pad(messageData, toSize: blockSize)
|
||||||
|
print("[PRIVACY] Padded message from \(messageData.count) to \(paddedData.count) bytes")
|
||||||
|
|
||||||
|
// Encrypt the padded message for the recipient
|
||||||
let encryptedPayload: Data
|
let encryptedPayload: Data
|
||||||
do {
|
do {
|
||||||
encryptedPayload = try self.encryptionService.encrypt(messageData, for: recipientPeerID)
|
encryptedPayload = try self.encryptionService.encrypt(paddedData, for: recipientPeerID)
|
||||||
print("[CRYPTO] Successfully encrypted private message for \(recipientPeerID)")
|
print("[CRYPTO] Successfully encrypted private message for \(recipientPeerID)")
|
||||||
} catch {
|
} catch {
|
||||||
print("[CRYPTO] Failed to encrypt private message: \(error)")
|
print("[CRYPTO] Failed to encrypt private message: \(error)")
|
||||||
@@ -376,7 +411,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Create packet with recipient ID for proper routing
|
// Create packet with recipient ID for proper routing
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.privateMessage.rawValue,
|
type: MessageType.message.rawValue,
|
||||||
senderID: Data(self.myPeerID.utf8),
|
senderID: Data(self.myPeerID.utf8),
|
||||||
recipientID: Data(recipientPeerID.utf8),
|
recipientID: Data(recipientPeerID.utf8),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970),
|
timestamp: UInt64(Date().timeIntervalSince1970),
|
||||||
@@ -386,7 +421,13 @@ class BluetoothMeshService: NSObject {
|
|||||||
)
|
)
|
||||||
|
|
||||||
print("[PRIVATE] Sending encrypted message to \(recipientPeerID): \(content)")
|
print("[PRIVATE] Sending encrypted message to \(recipientPeerID): \(content)")
|
||||||
self.broadcastPacket(packet)
|
|
||||||
|
// Add random delay for timing obfuscation
|
||||||
|
let delay = self.randomDelay()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
|
self?.broadcastPacket(packet)
|
||||||
|
print("[PRIVACY] Private message sent with \(Int(delay * 1000))ms delay")
|
||||||
|
}
|
||||||
|
|
||||||
// Don't call didReceiveMessage here - let the view model handle it directly
|
// Don't call didReceiveMessage here - let the view model handle it directly
|
||||||
}
|
}
|
||||||
@@ -515,7 +556,6 @@ class BluetoothMeshService: NSObject {
|
|||||||
guard packet.type != MessageType.keyExchange.rawValue,
|
guard packet.type != MessageType.keyExchange.rawValue,
|
||||||
packet.type != MessageType.announce.rawValue,
|
packet.type != MessageType.announce.rawValue,
|
||||||
packet.type != MessageType.leave.rawValue,
|
packet.type != MessageType.leave.rawValue,
|
||||||
packet.type != MessageType.ack.rawValue,
|
|
||||||
packet.type != MessageType.fragmentStart.rawValue,
|
packet.type != MessageType.fragmentStart.rawValue,
|
||||||
packet.type != MessageType.fragmentContinue.rawValue,
|
packet.type != MessageType.fragmentContinue.rawValue,
|
||||||
packet.type != MessageType.fragmentEnd.rawValue else {
|
packet.type != MessageType.fragmentEnd.rawValue else {
|
||||||
@@ -524,7 +564,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Check if this is a private message for a favorite
|
// Check if this is a private message for a favorite
|
||||||
var isForFavorite = false
|
var isForFavorite = false
|
||||||
if packet.type == MessageType.privateMessage.rawValue,
|
if packet.type == MessageType.message.rawValue,
|
||||||
let recipientID = packet.recipientID,
|
let recipientID = packet.recipientID,
|
||||||
let recipientPeerID = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) {
|
let recipientPeerID = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) {
|
||||||
// Check if recipient is a favorite via their public key fingerprint
|
// Check if recipient is a favorite via their public key fingerprint
|
||||||
@@ -612,9 +652,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
for (index, storedMessage) in messagesToSend.enumerated() {
|
for (index, storedMessage) in messagesToSend.enumerated() {
|
||||||
let delay = Double(index) * 0.1 // 100ms between messages
|
let delay = Double(index) * 0.1 // 100ms between messages
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, weak peripheral] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak peripheral] in
|
||||||
guard let self = self,
|
guard let peripheral = peripheral,
|
||||||
let peripheral = peripheral,
|
|
||||||
peripheral.state == .connected else { return }
|
peripheral.state == .connected else { return }
|
||||||
|
|
||||||
// Create a new packet with fresh timestamp
|
// Create a new packet with fresh timestamp
|
||||||
@@ -744,7 +783,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
switch MessageType(rawValue: packet.type) {
|
switch MessageType(rawValue: packet.type) {
|
||||||
case .message:
|
case .message:
|
||||||
// Process broadcast message (no decryption needed)
|
// Unified message handler for both broadcast and private messages
|
||||||
guard let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) else {
|
guard let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -754,45 +793,138 @@ class BluetoothMeshService: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify signature if present
|
// Check if this is a broadcast or private message
|
||||||
if let signature = packet.signature {
|
if let recipientID = packet.recipientID {
|
||||||
do {
|
if recipientID == SpecialRecipients.broadcast {
|
||||||
let isValid = try encryptionService.verify(signature, for: packet.payload, from: senderID)
|
// BROADCAST MESSAGE
|
||||||
if !isValid {
|
print("[MESSAGE] Received broadcast message")
|
||||||
print("[CRYPTO] Invalid signature from \(senderID), dropping message")
|
|
||||||
|
// Verify signature if present
|
||||||
|
if let signature = packet.signature {
|
||||||
|
do {
|
||||||
|
let isValid = try encryptionService.verify(signature, for: packet.payload, from: senderID)
|
||||||
|
if !isValid {
|
||||||
|
print("[CRYPTO] Invalid signature from \(senderID), dropping message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[CRYPTO] Failed to verify signature from \(senderID): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse broadcast message (not encrypted)
|
||||||
|
if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
|
||||||
|
print("[MESSAGE] Broadcast from \(message.sender): \(message.content)")
|
||||||
|
|
||||||
|
// Store nickname mapping
|
||||||
|
peerNicknames[senderID] = message.sender
|
||||||
|
|
||||||
|
let messageWithPeerID = BitchatMessage(
|
||||||
|
sender: message.sender,
|
||||||
|
content: message.content,
|
||||||
|
timestamp: message.timestamp,
|
||||||
|
isRelay: message.isRelay,
|
||||||
|
originalSender: message.originalSender,
|
||||||
|
isPrivate: false,
|
||||||
|
recipientNickname: nil,
|
||||||
|
senderPeerID: senderID,
|
||||||
|
mentions: message.mentions
|
||||||
|
)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.didReceiveMessage(messageWithPeerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relay if TTL > 0
|
||||||
|
var relayPacket = packet
|
||||||
|
relayPacket.ttl -= 1
|
||||||
|
if relayPacket.ttl > 0 {
|
||||||
|
self.cacheMessage(relayPacket, messageID: messageID)
|
||||||
|
self.broadcastPacket(relayPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if let recipientIDString = String(data: recipientID.trimmingNullBytes(), encoding: .utf8),
|
||||||
|
recipientIDString == myPeerID {
|
||||||
|
// PRIVATE MESSAGE FOR US
|
||||||
|
print("[MESSAGE] Received private message for us")
|
||||||
|
|
||||||
|
// Verify signature if present
|
||||||
|
if let signature = packet.signature {
|
||||||
|
do {
|
||||||
|
let isValid = try encryptionService.verify(signature, for: packet.payload, from: senderID)
|
||||||
|
if !isValid {
|
||||||
|
print("[CRYPTO] Invalid signature on private message from \(senderID), dropping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[CRYPTO] Failed to verify signature from \(senderID): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the message
|
||||||
|
let decryptedPayload: Data
|
||||||
|
do {
|
||||||
|
let decryptedPadded = try encryptionService.decrypt(packet.payload, from: senderID)
|
||||||
|
print("[CRYPTO] Successfully decrypted private message from \(senderID)")
|
||||||
|
|
||||||
|
// Remove padding
|
||||||
|
decryptedPayload = MessagePadding.unpad(decryptedPadded)
|
||||||
|
print("[PRIVACY] Unpadded message from \(decryptedPadded.count) to \(decryptedPayload.count) bytes")
|
||||||
|
} catch {
|
||||||
|
print("[CRYPTO] Failed to decrypt private message from \(senderID): \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Valid signature
|
|
||||||
} catch {
|
// Parse the decrypted message
|
||||||
print("[CRYPTO] Failed to verify signature from \(senderID): \(error)")
|
if let message = BitchatMessage.fromBinaryPayload(decryptedPayload) {
|
||||||
// If we don't have the public key yet, continue without verification
|
// Check if this is a dummy message for cover traffic
|
||||||
// Continuing without signature verification
|
if message.content.hasPrefix(self.coverTrafficPrefix) {
|
||||||
}
|
print("[PRIVACY] Received and discarded cover traffic from \(senderID)")
|
||||||
} else {
|
return // Silently discard dummy messages
|
||||||
print("[CRYPTO] No signature present in message from \(senderID)")
|
}
|
||||||
}
|
|
||||||
|
print("[MESSAGE] Private from \(senderID): \(message.content)")
|
||||||
let messagePayload = packet.payload
|
|
||||||
|
// Store nickname mapping if we don't have it
|
||||||
if let message = BitchatMessage.fromBinaryPayload(messagePayload) {
|
if peerNicknames[senderID] == nil {
|
||||||
print("[MESSAGE] Received from \(message.sender): \(message.content)")
|
peerNicknames[senderID] = message.sender
|
||||||
|
}
|
||||||
// Store nickname mapping
|
|
||||||
if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
|
let messageWithPeerID = BitchatMessage(
|
||||||
peerNicknames[senderID] = message.sender
|
sender: message.sender,
|
||||||
}
|
content: message.content,
|
||||||
|
timestamp: message.timestamp,
|
||||||
DispatchQueue.main.async {
|
isRelay: message.isRelay,
|
||||||
self.delegate?.didReceiveMessage(message)
|
originalSender: message.originalSender,
|
||||||
}
|
isPrivate: message.isPrivate,
|
||||||
|
recipientNickname: message.recipientNickname,
|
||||||
var relayPacket = packet
|
senderPeerID: senderID
|
||||||
relayPacket.ttl -= 1
|
)
|
||||||
if relayPacket.ttl > 0 {
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.didReceiveMessage(messageWithPeerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if packet.ttl > 0 {
|
||||||
|
// RELAY PRIVATE MESSAGE (not for us)
|
||||||
|
print("[MESSAGE] Relaying private message not meant for us (TTL: \(packet.ttl))")
|
||||||
|
var relayPacket = packet
|
||||||
|
relayPacket.ttl -= 1
|
||||||
|
|
||||||
|
// Check if this message is for an offline favorite and cache it
|
||||||
|
if let recipientIDString = String(data: recipientID.trimmingNullBytes(), encoding: .utf8),
|
||||||
|
let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientIDString) {
|
||||||
|
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
||||||
|
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
||||||
|
print("[CACHE] Caching relayed message for offline favorite: \(recipientIDString)")
|
||||||
|
self.cacheMessage(relayPacket, messageID: messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.broadcastPacket(relayPacket)
|
self.broadcastPacket(relayPacket)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
print("[MESSAGE] Failed to parse message from payload")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .keyExchange:
|
case .keyExchange:
|
||||||
@@ -944,105 +1076,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
print("[LEAVE] Failed to parse leave packet")
|
print("[LEAVE] Failed to parse leave packet")
|
||||||
}
|
}
|
||||||
|
|
||||||
case .privateMessage:
|
|
||||||
print("[PRIVATE] Received private message packet")
|
|
||||||
// Check if this private message is for us
|
|
||||||
if let recipientID = packet.recipientID,
|
|
||||||
let recipientIDString = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) {
|
|
||||||
print("[PRIVATE] Message recipient: \(recipientIDString), myPeerID: \(myPeerID)")
|
|
||||||
|
|
||||||
if recipientIDString == myPeerID {
|
|
||||||
// Get sender ID
|
|
||||||
if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
|
|
||||||
// Ignore our own messages
|
|
||||||
if senderID == myPeerID {
|
|
||||||
print("[PRIVATE] Ignoring own message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signature if present
|
|
||||||
if let signature = packet.signature {
|
|
||||||
do {
|
|
||||||
let isValid = try encryptionService.verify(signature, for: packet.payload, from: senderID)
|
|
||||||
if !isValid {
|
|
||||||
print("[CRYPTO] Invalid signature on private message from \(senderID), dropping")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
print("[CRYPTO] Valid signature on private message from \(senderID)")
|
|
||||||
} catch {
|
|
||||||
print("[CRYPTO] Failed to verify signature from \(senderID): \(error)")
|
|
||||||
// Continue without signature verification for now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt the message
|
|
||||||
let decryptedPayload: Data
|
|
||||||
do {
|
|
||||||
decryptedPayload = try encryptionService.decrypt(packet.payload, from: senderID)
|
|
||||||
print("[CRYPTO] Successfully decrypted private message from \(senderID)")
|
|
||||||
} catch {
|
|
||||||
print("[CRYPTO] Failed to decrypt private message from \(senderID): \(error)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the decrypted message
|
|
||||||
if let message = BitchatMessage.fromBinaryPayload(decryptedPayload) {
|
|
||||||
print("[PRIVATE] Received private message from \(senderID): \(message.content)")
|
|
||||||
|
|
||||||
// Store nickname mapping if we don't have it
|
|
||||||
if peerNicknames[senderID] == nil {
|
|
||||||
peerNicknames[senderID] = message.sender
|
|
||||||
|
|
||||||
// Update peer list to show the new nickname
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new message with the sender peer ID
|
|
||||||
let messageWithPeerID = BitchatMessage(
|
|
||||||
sender: message.sender,
|
|
||||||
content: message.content,
|
|
||||||
timestamp: message.timestamp,
|
|
||||||
isRelay: message.isRelay,
|
|
||||||
originalSender: message.originalSender,
|
|
||||||
isPrivate: message.isPrivate,
|
|
||||||
recipientNickname: message.recipientNickname,
|
|
||||||
senderPeerID: senderID
|
|
||||||
)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.delegate?.didReceiveMessage(messageWithPeerID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("[PRIVATE] Failed to parse decrypted message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if packet.ttl > 0 {
|
|
||||||
// Relay private messages that aren't for us
|
|
||||||
print("[PRIVATE] Relaying message not meant for us (TTL: \(packet.ttl))")
|
|
||||||
var relayPacket = packet
|
|
||||||
relayPacket.ttl -= 1
|
|
||||||
|
|
||||||
// Check if this message is for an offline favorite and cache it
|
|
||||||
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientIDString) {
|
|
||||||
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
|
||||||
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
|
||||||
// This is for a favorite peer - cache it even if they're offline
|
|
||||||
print("[CACHE] Caching relayed message for offline favorite: \(recipientIDString)")
|
|
||||||
self.cacheMessage(relayPacket, messageID: messageID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.broadcastPacket(relayPacket)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("[PRIVATE] No recipient ID in packet")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
case .fragmentStart, .fragmentContinue, .fragmentEnd:
|
case .fragmentStart, .fragmentContinue, .fragmentEnd:
|
||||||
let fragmentTypeStr = packet.type == 10 ? "START" : (packet.type == 11 ? "CONTINUE" : "END")
|
let fragmentTypeStr = packet.type == MessageType.fragmentStart.rawValue ? "START" :
|
||||||
|
(packet.type == MessageType.fragmentContinue.rawValue ? "CONTINUE" : "END")
|
||||||
print("[PACKET] Handling fragment type: \(fragmentTypeStr) (\(packet.type)), payload size: \(packet.payload.count), from: \(peerID)")
|
print("[PACKET] Handling fragment type: \(fragmentTypeStr) (\(packet.type)), payload size: \(packet.payload.count), from: \(peerID)")
|
||||||
|
|
||||||
// Validate fragment has minimum required size
|
// Validate fragment has minimum required size
|
||||||
@@ -1715,4 +1751,77 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
scheduleScanDutyCycle()
|
scheduleScanDutyCycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Privacy Utilities
|
||||||
|
|
||||||
|
private func randomDelay() -> TimeInterval {
|
||||||
|
// Generate random delay between min and max for timing obfuscation
|
||||||
|
return TimeInterval.random(in: minMessageDelay...maxMessageDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cover Traffic
|
||||||
|
|
||||||
|
private func startCoverTraffic() {
|
||||||
|
// Start cover traffic with random interval
|
||||||
|
scheduleCoverTraffic()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleCoverTraffic() {
|
||||||
|
// Random interval between 30-120 seconds
|
||||||
|
let interval = TimeInterval.random(in: 30...120)
|
||||||
|
|
||||||
|
coverTrafficTimer?.invalidate()
|
||||||
|
coverTrafficTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
|
||||||
|
self?.sendDummyMessage()
|
||||||
|
self?.scheduleCoverTraffic() // Schedule next dummy message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendDummyMessage() {
|
||||||
|
// Only send dummy messages if we have connected peers
|
||||||
|
let peers = getAllConnectedPeerIDs()
|
||||||
|
guard !peers.isEmpty else { return }
|
||||||
|
|
||||||
|
// Skip if battery is low
|
||||||
|
if currentBatteryLevel < 0.2 {
|
||||||
|
print("[PRIVACY] Skipping cover traffic due to low battery")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a random peer to send to
|
||||||
|
guard let randomPeer = peers.randomElement() else { return }
|
||||||
|
|
||||||
|
// Generate random dummy content
|
||||||
|
let dummyContent = generateDummyContent()
|
||||||
|
|
||||||
|
print("[PRIVACY] Sending cover traffic to \(randomPeer)")
|
||||||
|
|
||||||
|
// Send as a private message so it's encrypted
|
||||||
|
sendPrivateMessage(dummyContent, to: randomPeer, recipientNickname: peerNicknames[randomPeer] ?? "unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateDummyContent() -> String {
|
||||||
|
// Generate realistic-looking dummy messages
|
||||||
|
let templates = [
|
||||||
|
"hey",
|
||||||
|
"ok",
|
||||||
|
"got it",
|
||||||
|
"sure",
|
||||||
|
"sounds good",
|
||||||
|
"thanks",
|
||||||
|
"np",
|
||||||
|
"see you there",
|
||||||
|
"on my way",
|
||||||
|
"running late",
|
||||||
|
"be there soon",
|
||||||
|
"👍",
|
||||||
|
"✓",
|
||||||
|
"meeting at the usual spot",
|
||||||
|
"confirmed",
|
||||||
|
"roger that"
|
||||||
|
]
|
||||||
|
|
||||||
|
// Prefix with dummy marker (will be encrypted)
|
||||||
|
return coverTrafficPrefix + (templates.randomElement() ?? "ok")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -35,9 +35,7 @@ class ChatViewModel: ObservableObject {
|
|||||||
@Published var favoritePeers: Set<String> = [] // Now stores public key fingerprints instead of peer IDs
|
@Published var favoritePeers: Set<String> = [] // Now stores public key fingerprints instead of peer IDs
|
||||||
private var peerIDToPublicKeyFingerprint: [String: String] = [:] // Maps ephemeral peer IDs to persistent fingerprints
|
private var peerIDToPublicKeyFingerprint: [String: String] = [:] // Maps ephemeral peer IDs to persistent fingerprints
|
||||||
|
|
||||||
// Ephemeral message settings
|
// Messages are naturally ephemeral - no persistent storage
|
||||||
private var messageAutoDeleteTimer: Timer?
|
|
||||||
private let messageRetentionTime: TimeInterval = 300 // 5 minutes
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
loadNickname()
|
loadNickname()
|
||||||
@@ -49,9 +47,6 @@ class ChatViewModel: ObservableObject {
|
|||||||
|
|
||||||
// Request notification permission
|
// Request notification permission
|
||||||
NotificationService.shared.requestAuthorization()
|
NotificationService.shared.requestAuthorization()
|
||||||
|
|
||||||
// Start auto-delete timer for ephemeral messages
|
|
||||||
startAutoDeleteTimer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadNickname() {
|
private func loadNickname() {
|
||||||
@@ -225,37 +220,6 @@ class ChatViewModel: ObservableObject {
|
|||||||
print("[PANIC] All data cleared for safety")
|
print("[PANIC] All data cleared for safety")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ephemeral message auto-deletion
|
|
||||||
private func startAutoDeleteTimer() {
|
|
||||||
messageAutoDeleteTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in
|
|
||||||
self?.deleteOldMessages()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func deleteOldMessages() {
|
|
||||||
let cutoffTime = Date().addingTimeInterval(-messageRetentionTime)
|
|
||||||
|
|
||||||
// Delete old public messages
|
|
||||||
let beforeCount = messages.count
|
|
||||||
messages.removeAll { message in
|
|
||||||
message.timestamp < cutoffTime && message.sender != "system"
|
|
||||||
}
|
|
||||||
|
|
||||||
if messages.count < beforeCount {
|
|
||||||
print("[EPHEMERAL] Deleted \(beforeCount - messages.count) old messages")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete old private messages
|
|
||||||
for (peerID, messageList) in privateChats {
|
|
||||||
let oldCount = messageList.count
|
|
||||||
privateChats[peerID] = messageList.filter { $0.timestamp >= cutoffTime }
|
|
||||||
|
|
||||||
if let newCount = privateChats[peerID]?.count, newCount < oldCount {
|
|
||||||
print("[EPHEMERAL] Deleted \(oldCount - newCount) old private messages from \(peerID)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func formatTimestamp(_ date: Date) -> String {
|
func formatTimestamp(_ date: Date) -> String {
|
||||||
|
|||||||
@@ -59,9 +59,6 @@ struct AppInfoView: View {
|
|||||||
FeatureRow(icon: "antenna.radiowaves.left.and.right", title: "Extended Range",
|
FeatureRow(icon: "antenna.radiowaves.left.and.right", title: "Extended Range",
|
||||||
description: "Messages relay through peers, reaching 300m+")
|
description: "Messages relay through peers, reaching 300m+")
|
||||||
|
|
||||||
FeatureRow(icon: "clock.arrow.circlepath", title: "Ephemeral Messages",
|
|
||||||
description: "Messages auto-delete after 5 minutes")
|
|
||||||
|
|
||||||
FeatureRow(icon: "star.fill", title: "Favorites System",
|
FeatureRow(icon: "star.fill", title: "Favorites System",
|
||||||
description: "Store-and-forward messages for favorites indefinitely")
|
description: "Store-and-forward messages for favorites indefinitely")
|
||||||
|
|
||||||
@@ -159,9 +156,6 @@ struct AppInfoView: View {
|
|||||||
FeatureRow(icon: "antenna.radiowaves.left.and.right", title: "Extended Range",
|
FeatureRow(icon: "antenna.radiowaves.left.and.right", title: "Extended Range",
|
||||||
description: "Messages relay through peers, reaching 300m+")
|
description: "Messages relay through peers, reaching 300m+")
|
||||||
|
|
||||||
FeatureRow(icon: "clock.arrow.circlepath", title: "Ephemeral Messages",
|
|
||||||
description: "Messages auto-delete after 5 minutes")
|
|
||||||
|
|
||||||
FeatureRow(icon: "star.fill", title: "Favorites System",
|
FeatureRow(icon: "star.fill", title: "Favorites System",
|
||||||
description: "Store-and-forward messages for favorites indefinitely")
|
description: "Store-and-forward messages for favorites indefinitely")
|
||||||
|
|
||||||
|
|||||||
@@ -158,10 +158,15 @@ struct ContentView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text("private: \(privatePeerNick)")
|
HStack(spacing: 6) {
|
||||||
.font(.system(size: 16, weight: .medium, design: .monospaced))
|
Image(systemName: "lock.fill")
|
||||||
.foregroundColor(Color.orange)
|
.font(.system(size: 14))
|
||||||
.frame(maxWidth: .infinity)
|
.foregroundColor(Color.orange)
|
||||||
|
Text("private: \(privatePeerNick)")
|
||||||
|
.font(.system(size: 16, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundColor(Color.orange)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
# BitChat Security and Encryption Analysis
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
BitChat is a Bluetooth mesh networking app that implements a mix of encrypted and unencrypted communications. While private messages are properly encrypted using Curve25519 and AES-GCM, public broadcast messages are sent in plaintext with optional signatures. The app has several security strengths but also notable vulnerabilities that could compromise user privacy and security.
|
||||||
|
|
||||||
|
## 1. Message Encryption
|
||||||
|
|
||||||
|
### 1.1 Encrypted Messages
|
||||||
|
- **Private Messages**: Properly encrypted using Curve25519 key agreement and AES-GCM
|
||||||
|
- Uses ephemeral key pairs for forward secrecy
|
||||||
|
- Implements proper authenticated encryption (AEAD)
|
||||||
|
- Encrypted payload includes the full message content
|
||||||
|
|
||||||
|
### 1.2 Unencrypted Messages
|
||||||
|
- **Public/Broadcast Messages**: Sent in **PLAINTEXT**
|
||||||
|
- Message content, sender nickname, timestamps are all visible
|
||||||
|
- Only protected by optional signatures (not encryption)
|
||||||
|
- Anyone within Bluetooth range can read these messages
|
||||||
|
- **Announce Messages**: Sent in plaintext containing nicknames
|
||||||
|
- **Key Exchange Messages**: Public keys sent in plaintext (this is acceptable)
|
||||||
|
|
||||||
|
### 1.3 Partially Protected Data
|
||||||
|
- **Fragments**: Large messages are fragmented but fragments themselves are not encrypted unless the original message was private
|
||||||
|
- **Metadata**: TTL, timestamps, sender/recipient IDs are always in plaintext
|
||||||
|
|
||||||
|
## 2. Key Management
|
||||||
|
|
||||||
|
### 2.1 Key Types
|
||||||
|
The app uses three types of keys per peer:
|
||||||
|
1. **Ephemeral Encryption Key** (Curve25519 KeyAgreement) - Changes each session
|
||||||
|
2. **Ephemeral Signing Key** (Curve25519 Signing) - Changes each session
|
||||||
|
3. **Persistent Identity Key** (Curve25519 Signing) - Stored in UserDefaults
|
||||||
|
|
||||||
|
### 2.2 Key Exchange Process
|
||||||
|
```
|
||||||
|
1. On connection, peers exchange 96 bytes containing:
|
||||||
|
- 32 bytes: Ephemeral encryption public key
|
||||||
|
- 32 bytes: Ephemeral signing public key
|
||||||
|
- 32 bytes: Persistent identity public key
|
||||||
|
|
||||||
|
2. Shared secrets are derived using HKDF with:
|
||||||
|
- Salt: "bitchat-v1"
|
||||||
|
- Info: empty
|
||||||
|
- Output: 32-byte symmetric key for AES-GCM
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Key Storage Vulnerabilities
|
||||||
|
- **Persistent identity keys** stored in UserDefaults (not secure storage)
|
||||||
|
- No key rotation mechanism for persistent keys
|
||||||
|
- No key expiration or revocation support
|
||||||
|
- Ephemeral keys provide forward secrecy but are lost on app restart
|
||||||
|
|
||||||
|
## 3. Authentication & Signatures
|
||||||
|
|
||||||
|
### 3.1 Message Authentication
|
||||||
|
- Messages can be signed using ephemeral signing keys
|
||||||
|
- Signatures use Curve25519 (Ed25519) - cryptographically strong
|
||||||
|
- **CRITICAL ISSUE**: Signatures are optional, not mandatory
|
||||||
|
- Broadcast messages often sent without signatures
|
||||||
|
- No enforcement of signature verification
|
||||||
|
|
||||||
|
### 3.2 Identity Verification
|
||||||
|
- No mechanism to verify persistent identity keys
|
||||||
|
- Peer IDs are random 8-character hex strings (ephemeral per session)
|
||||||
|
- Nicknames are self-assigned and not authenticated
|
||||||
|
- **Impersonation Risk**: Anyone can claim any nickname
|
||||||
|
|
||||||
|
### 3.3 Anti-Replay Protection
|
||||||
|
- Basic timestamp validation (5-minute window)
|
||||||
|
- Message deduplication based on timestamp + sender ID
|
||||||
|
- **Weakness**: Deduplication cache cleared after 1000 messages
|
||||||
|
|
||||||
|
## 4. Privacy Analysis
|
||||||
|
|
||||||
|
### 4.1 Metadata Exposure
|
||||||
|
The following metadata is **always exposed** in plaintext:
|
||||||
|
- Message type (broadcast, private, announce, etc.)
|
||||||
|
- Timestamp (exact time of message)
|
||||||
|
- TTL (time-to-live) value
|
||||||
|
- Sender ID (8-character ephemeral ID)
|
||||||
|
- Recipient ID (for private messages)
|
||||||
|
- Message exists (traffic analysis possible)
|
||||||
|
|
||||||
|
### 4.2 User Tracking
|
||||||
|
- **Session Tracking**: Ephemeral peer IDs change per session (good)
|
||||||
|
- **Long-term Tracking**: Persistent identity keys enable tracking favorites across sessions
|
||||||
|
- **Nickname Tracking**: Self-assigned nicknames can be tracked
|
||||||
|
- **RSSI Tracking**: Signal strength logged, enabling location tracking
|
||||||
|
|
||||||
|
### 4.3 Traffic Analysis Vulnerabilities
|
||||||
|
- Message sizes not padded (reveals content length)
|
||||||
|
- Timing patterns not obscured
|
||||||
|
- Relay behavior reveals network topology
|
||||||
|
- Fragment reassembly reveals large message senders
|
||||||
|
|
||||||
|
## 5. Security Vulnerabilities
|
||||||
|
|
||||||
|
### 5.1 MITM (Man-in-the-Middle) Attacks
|
||||||
|
- **Key Exchange Vulnerable**: No authentication during initial key exchange
|
||||||
|
- Anyone can intercept and replace public keys
|
||||||
|
- No certificate pinning or trust verification
|
||||||
|
- **Mitigation**: Only persistent identity keys provide some continuity
|
||||||
|
|
||||||
|
### 5.2 Replay Attack Protection
|
||||||
|
- **Partial Protection**: 5-minute timestamp window
|
||||||
|
- **Weakness**: Attacker can replay within window
|
||||||
|
- **Weakness**: Cache-based deduplication can be overwhelmed
|
||||||
|
|
||||||
|
### 5.3 Key Compromise Impact
|
||||||
|
- **Ephemeral Key Compromise**: Only affects current session
|
||||||
|
- **Identity Key Compromise**: Affects all future favorite communications
|
||||||
|
- **No Perfect Forward Secrecy** for identity-based communications
|
||||||
|
|
||||||
|
### 5.4 Message Integrity
|
||||||
|
- **Private Messages**: Protected by AES-GCM authentication tag
|
||||||
|
- **Public Messages**: Only protected if signed (optional)
|
||||||
|
- **Fragments**: No integrity protection during reassembly
|
||||||
|
|
||||||
|
### 5.5 Denial of Service
|
||||||
|
- No rate limiting on messages
|
||||||
|
- Fragment reassembly can consume memory
|
||||||
|
- Message cache can be filled with spam
|
||||||
|
- TTL-based flooding possible
|
||||||
|
|
||||||
|
## 6. Protocol-Specific Vulnerabilities
|
||||||
|
|
||||||
|
### 6.1 Binary Protocol Issues
|
||||||
|
- No protocol version negotiation
|
||||||
|
- Fixed-size fields can lead to truncation
|
||||||
|
- No extension mechanism for future security features
|
||||||
|
|
||||||
|
### 6.2 Bluetooth-Specific Risks
|
||||||
|
- BLE advertisements reveal app usage
|
||||||
|
- Connection attempts logged by OS
|
||||||
|
- RSSI measurements enable physical tracking
|
||||||
|
- No protection against Bluetooth protocol attacks
|
||||||
|
|
||||||
|
## 7. Implementation Issues
|
||||||
|
|
||||||
|
### 7.1 Cryptographic Issues
|
||||||
|
- Using SHA256 for fingerprints (should use key-specific hashing)
|
||||||
|
- No constant-time comparisons for signatures
|
||||||
|
- Error messages may leak timing information
|
||||||
|
|
||||||
|
### 7.2 Memory Safety
|
||||||
|
- Message cache stores decrypted content
|
||||||
|
- No secure memory wiping after use
|
||||||
|
- Crash dumps may contain sensitive data
|
||||||
|
|
||||||
|
## 8. Recommendations
|
||||||
|
|
||||||
|
### 8.1 Critical Fixes
|
||||||
|
1. **Encrypt all messages** including broadcasts
|
||||||
|
2. **Mandatory signatures** on all messages
|
||||||
|
3. **Authenticated key exchange** (e.g., using SMP or custom protocol)
|
||||||
|
4. **Secure key storage** using Keychain instead of UserDefaults
|
||||||
|
|
||||||
|
### 8.2 Privacy Enhancements
|
||||||
|
1. **Pad message sizes** to fixed buckets
|
||||||
|
2. **Add decoy traffic** to obscure patterns
|
||||||
|
3. **Randomize timing** of message relay
|
||||||
|
4. **Implement onion routing** for multi-hop messages
|
||||||
|
|
||||||
|
### 8.3 Security Improvements
|
||||||
|
1. **Add perfect forward secrecy** for all messages
|
||||||
|
2. **Implement key rotation** for long-term keys
|
||||||
|
3. **Add replay protection** with sequence numbers
|
||||||
|
4. **Rate limiting** to prevent DoS attacks
|
||||||
|
|
||||||
|
### 8.4 Protocol Enhancements
|
||||||
|
1. **Version negotiation** for protocol upgrades
|
||||||
|
2. **Capability advertisement** for feature discovery
|
||||||
|
3. **Extension fields** for future features
|
||||||
|
4. **Formal security audit** of protocol design
|
||||||
|
|
||||||
|
## 9. Threat Model Considerations
|
||||||
|
|
||||||
|
### 9.1 Local Adversary (Within Bluetooth Range)
|
||||||
|
- Can read all broadcast messages
|
||||||
|
- Can perform traffic analysis
|
||||||
|
- Can attempt MITM during key exchange
|
||||||
|
- Can track users via RSSI
|
||||||
|
|
||||||
|
### 9.2 Network Adversary (Multiple Nodes)
|
||||||
|
- Can correlate messages across the mesh
|
||||||
|
- Can map network topology
|
||||||
|
- Can perform timing correlation attacks
|
||||||
|
- Can identify high-value targets (favorites)
|
||||||
|
|
||||||
|
### 9.3 Persistent Adversary
|
||||||
|
- Can track users across sessions via identity keys
|
||||||
|
- Can build social graphs from message patterns
|
||||||
|
- Can perform long-term traffic analysis
|
||||||
|
- Can compromise stored keys from UserDefaults
|
||||||
|
|
||||||
|
## 10. Conclusion
|
||||||
|
|
||||||
|
BitChat implements basic encryption for private messages but has significant security and privacy vulnerabilities. The lack of encryption for broadcast messages, optional signatures, vulnerable key exchange, and metadata exposure make it unsuitable for high-security scenarios. While the app provides some protection against casual eavesdropping, it would not withstand targeted attacks by motivated adversaries.
|
||||||
|
|
||||||
|
For activist or high-risk use cases, the current implementation poses serious risks including:
|
||||||
|
- Message content exposure (broadcasts)
|
||||||
|
- User tracking and identification
|
||||||
|
- Social graph analysis
|
||||||
|
- Physical location tracking via RSSI
|
||||||
|
|
||||||
|
Major architectural changes would be needed to provide adequate security for sensitive communications.
|
||||||
Reference in New Issue
Block a user