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:
jack
2025-07-04 11:53:57 +02:00
parent cce43fcfc7
commit f77cec3fb2
6 changed files with 545 additions and 213 deletions
+64 -11
View File
@@ -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 {
+264 -155
View File
@@ -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")
}
} }
+1 -37
View File
@@ -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 {
-6
View File
@@ -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")
+9 -4
View File
@@ -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()
+207
View File
@@ -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.