diff --git a/bitchat/Protocols/BitchatProtocol.swift b/bitchat/Protocols/BitchatProtocol.swift index 9f738ca3..049a9b7c 100644 --- a/bitchat/Protocols/BitchatProtocol.swift +++ b/bitchat/Protocols/BitchatProtocol.swift @@ -92,4 +92,7 @@ protocol BitchatDelegate: AnyObject { func didConnectToPeer(_ peerID: String) func didDisconnectFromPeer(_ peerID: String) func didUpdatePeerList(_ peers: [String]) + + // Optional method to check if a fingerprint belongs to a favorite peer + @objc optional func isFavorite(fingerprint: String) -> Bool } \ No newline at end of file diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 8423a17e..c640d5f7 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -1,6 +1,7 @@ import Foundation import CoreBluetooth import Combine +import CryptoKit #if os(macOS) import AppKit #else @@ -37,10 +38,23 @@ class BluetoothMeshService: NSObject { private let encryptionService = EncryptionService() private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent) private var processedMessages = Set() - private let maxTTL: UInt8 = 3 // Reduced for efficiency + private let maxTTL: UInt8 = 5 // Increased for better reach private var announcedToPeers = Set() // Track which peers we've announced to private var announcedPeers = Set() // Track peers who have already been announced + // Store-and-forward message cache + private struct StoredMessage { + let packet: BitchatPacket + let timestamp: Date + let messageID: String + let isForFavorite: Bool // Messages for favorites stored indefinitely + } + private var messageCache: [StoredMessage] = [] + private let messageCacheTimeout: TimeInterval = 43200 // 12 hours for regular peers + private let maxCachedMessages = 100 // For regular peers + private let maxCachedMessagesForFavorites = 1000 // Much larger cache for favorites + private var favoriteMessageQueue: [String: [StoredMessage]] = [:] // Per-favorite message queues + // Battery and range optimizations private var scanDutyCycleTimer: Timer? private var isActivelyScanning = true @@ -55,6 +69,16 @@ class BluetoothMeshService: NSObject { let myPeerID: String + // Helper method to get fingerprint from public key data + private func getPublicKeyFingerprint(_ publicKeyData: Data) -> String { + let fingerprint = SHA256.hash(data: publicKeyData) + .compactMap { String(format: "%02x", $0) } + .joined() + .prefix(16) // Use first 16 chars for brevity + .lowercased() + return String(fingerprint) + } + override init() { // Generate ephemeral peer ID for each session to prevent tracking // Use random bytes instead of UUID for better anonymity @@ -475,6 +499,131 @@ class BluetoothMeshService: NSObject { return Array(announcedPeers).sorted() } + // MARK: - Store-and-Forward Methods + + private func cacheMessage(_ packet: BitchatPacket, messageID: String) { + messageQueue.async(flags: .barrier) { [weak self] in + guard let self = self else { return } + + // Don't cache certain message types + guard packet.type != MessageType.keyExchange.rawValue, + packet.type != MessageType.announce.rawValue, + packet.type != MessageType.leave.rawValue, + packet.type != MessageType.ack.rawValue, + packet.type != MessageType.fragmentStart.rawValue, + packet.type != MessageType.fragmentContinue.rawValue, + packet.type != MessageType.fragmentEnd.rawValue else { + return + } + + // Check if this is a private message for a favorite + var isForFavorite = false + if packet.type == MessageType.privateMessage.rawValue, + let recipientID = packet.recipientID, + let recipientPeerID = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) { + // Check if recipient is a favorite via their public key fingerprint + if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientPeerID) { + let fingerprint = self.getPublicKeyFingerprint(publicKeyData) + isForFavorite = self.delegate?.isFavorite?(fingerprint: fingerprint) ?? false + } + } + + // Create stored message + let storedMessage = StoredMessage( + packet: packet, + timestamp: Date(), + messageID: messageID, + isForFavorite: isForFavorite + ) + + if isForFavorite { + // Store in favorite-specific queue + if let recipientID = packet.recipientID, + let recipientPeerID = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) { + if self.favoriteMessageQueue[recipientPeerID] == nil { + self.favoriteMessageQueue[recipientPeerID] = [] + } + self.favoriteMessageQueue[recipientPeerID]?.append(storedMessage) + + // Limit favorite queue size + if let count = self.favoriteMessageQueue[recipientPeerID]?.count, + count > self.maxCachedMessagesForFavorites { + self.favoriteMessageQueue[recipientPeerID]?.removeFirst() + } + + print("[CACHE] Cached message for favorite \(recipientPeerID), queue size: \(self.favoriteMessageQueue[recipientPeerID]?.count ?? 0)") + } + } else { + // Clean up old messages first (only for regular cache) + self.cleanupMessageCache() + + // Add to regular cache + self.messageCache.append(storedMessage) + + // Limit cache size + if self.messageCache.count > self.maxCachedMessages { + self.messageCache.removeFirst() + } + + print("[CACHE] Cached message (type: \(packet.type)), cache size: \(self.messageCache.count)") + } + } + } + + private func cleanupMessageCache() { + let cutoffTime = Date().addingTimeInterval(-messageCacheTimeout) + // Only remove non-favorite messages that are older than timeout + messageCache.removeAll { !$0.isForFavorite && $0.timestamp < cutoffTime } + } + + private func sendCachedMessages(to peerID: String) { + messageQueue.async { [weak self] in + guard let self = self, + let peripheral = self.connectedPeripherals[peerID], + let characteristic = self.peripheralCharacteristics[peripheral] else { + return + } + + // Clean up old messages first + self.cleanupMessageCache() + + var messagesToSend: [StoredMessage] = [] + + // First, check if this peer has any favorite messages waiting + if let favoriteMessages = self.favoriteMessageQueue[peerID] { + messagesToSend.append(contentsOf: favoriteMessages) + // Clear the favorite queue after adding to send list + self.favoriteMessageQueue[peerID] = nil + print("[CACHE] Found \(favoriteMessages.count) favorite messages for \(peerID)") + } + + // Then add regular cached messages + messagesToSend.append(contentsOf: self.messageCache) + + print("[CACHE] Sending \(messagesToSend.count) total cached messages to \(peerID)") + + // Send cached messages with slight delay between each + for (index, storedMessage) in messagesToSend.enumerated() { + let delay = Double(index) * 0.1 // 100ms between messages + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, weak peripheral] in + guard let self = self, + let peripheral = peripheral, + peripheral.state == .connected else { return } + + // Create a new packet with fresh timestamp + var packet = storedMessage.packet + packet.timestamp = UInt64(Date().timeIntervalSince1970) + + if let data = packet.toBinaryData() { + peripheral.writeValue(data, for: characteristic, type: .withoutResponse) + print("[CACHE] Sent cached message \(index + 1)/\(messagesToSend.count) to \(peerID)") + } + } + } + } + } + private func estimateDistance(rssi: Int) -> Int { // Rough distance estimation based on RSSI // Using path loss formula: RSSI = TxPower - 10 * n * log10(distance) @@ -683,6 +832,9 @@ class BluetoothMeshService: NSObject { // Send announce with our nickname immediately print("[KEY_EXCHANGE] Calling sendAnnouncementToPeer for \(senderID)") self.sendAnnouncementToPeer(senderID) + + // Check if this peer has cached messages (especially for favorites) + self.sendCachedMessages(to: senderID) } } @@ -724,9 +876,22 @@ class BluetoothMeshService: NSObject { // Check if this is a favorite peer and send notification // Note: This might not work immediately if key exchange hasn't happened yet - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - if let viewModel = self.delegate as? ChatViewModel, - viewModel.isFavorite(peerID: senderID) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self = self else { return } + + // Check if this is a favorite using their public key fingerprint + if let publicKeyData = self.encryptionService.getPeerIdentityKey(senderID) { + let fingerprint = self.getPublicKeyFingerprint(publicKeyData) + if self.delegate?.isFavorite?(fingerprint: fingerprint) ?? false { + NotificationService.shared.sendFavoriteOnlineNotification(nickname: nickname) + + // Send any cached messages for this favorite + print("[CACHE] Favorite peer \(senderID) came online, checking for cached messages") + self.sendCachedMessages(to: senderID) + } + } else if let viewModel = self.delegate as? ChatViewModel, + viewModel.isFavorite(peerID: senderID) { + // Fallback for backwards compatibility NotificationService.shared.sendFavoriteOnlineNotification(nickname: nickname) } } @@ -845,6 +1010,17 @@ class BluetoothMeshService: NSObject { 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 { diff --git a/bitchat/Services/EncryptionService.swift b/bitchat/Services/EncryptionService.swift index 0f8792c4..71964c58 100644 --- a/bitchat/Services/EncryptionService.swift +++ b/bitchat/Services/EncryptionService.swift @@ -51,43 +51,30 @@ class EncryptionService { // Add peer's combined public keys func addPeerPublicKey(_ peerID: String, publicKeyData: Data) throws { - print("[CRYPTO] Received public key data of size: \(publicKeyData.count)") - // Convert to array for safe access let keyBytes = [UInt8](publicKeyData) - if keyBytes.count == 96 { - // New format: 32 for key agreement + 32 for signing + 32 for identity - let keyAgreementData = Data(keyBytes[0..<32]) - let signingKeyData = Data(keyBytes[32..<64]) - let identityKeyData = Data(keyBytes[64..<96]) - - let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyAgreementData) - peerPublicKeys[peerID] = publicKey - - let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingKeyData) - peerSigningKeys[peerID] = signingKey - - let identityKey = try Curve25519.Signing.PublicKey(rawRepresentation: identityKeyData) - peerIdentityKeys[peerID] = identityKey - - print("[CRYPTO] Stored all three keys for peer \(peerID)") - } else if keyBytes.count == 64 { - // Handle old format (64 bytes) for backward compatibility - print("[CRYPTO] Received old key format (64 bytes), no identity key") - let keyAgreementData = Data(keyBytes[0..<32]) - let signingKeyData = Data(keyBytes[32..<64]) - - let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyAgreementData) - peerPublicKeys[peerID] = publicKey - - let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingKeyData) - peerSigningKeys[peerID] = signingKey - } else { - print("[CRYPTO] Invalid public key data size: \(keyBytes.count)") + guard keyBytes.count == 96 else { + print("[CRYPTO] Invalid public key data size: \(keyBytes.count), expected 96") throw EncryptionError.invalidPublicKey } + // Extract all three keys: 32 for key agreement + 32 for signing + 32 for identity + let keyAgreementData = Data(keyBytes[0..<32]) + let signingKeyData = Data(keyBytes[32..<64]) + let identityKeyData = Data(keyBytes[64..<96]) + + let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyAgreementData) + peerPublicKeys[peerID] = publicKey + + let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingKeyData) + peerSigningKeys[peerID] = signingKey + + let identityKey = try Curve25519.Signing.PublicKey(rawRepresentation: identityKeyData) + peerIdentityKeys[peerID] = identityKey + + print("[CRYPTO] Stored all three keys for peer \(peerID)") + // Generate shared secret for encryption if let publicKey = peerPublicKeys[peerID] { let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey) diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index cfbca732..cbe958c9 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -652,4 +652,8 @@ extension ChatViewModel: BitchatDelegate { return Array(Set(mentions)) // Remove duplicates } + func isFavorite(fingerprint: String) -> Bool { + return favoritePeers.contains(fingerprint) + } + } \ No newline at end of file