diff --git a/bitchat/Protocols/BinaryEncodingUtils.swift b/bitchat/Protocols/BinaryEncodingUtils.swift index 08c53130..5d7ef4ee 100644 --- a/bitchat/Protocols/BinaryEncodingUtils.swift +++ b/bitchat/Protocols/BinaryEncodingUtils.swift @@ -210,6 +210,15 @@ extension Data { return result.uppercased() } + + func readFixedBytes(at offset: inout Int, count: Int) -> Data? { + guard offset + count <= self.count else { return nil } + + let data = self[offset.. DeliveryAck? { + // Create defensive copy + let dataCopy = Data(data) + // Minimum size: 2 UUIDs (32) + recipientID (8) + hopCount (1) + timestamp (8) + min nickname - guard data.count >= 50 else { return nil } + guard dataCopy.count >= 50 else { return nil } var offset = 0 - guard let originalMessageID = data.readUUID(at: &offset), - let ackID = data.readUUID(at: &offset) else { return nil } + guard let originalMessageID = dataCopy.readUUID(at: &offset), + let ackID = dataCopy.readUUID(at: &offset) else { return nil } - guard offset + 8 <= data.count else { return nil } - let recipientIDData = data[offset.. ReadReceipt? { + // Create defensive copy + let dataCopy = Data(data) + // Minimum size: 2 UUIDs (32) + readerID (8) + timestamp (8) + min nickname - guard data.count >= 49 else { return nil } + guard dataCopy.count >= 49 else { return nil } var offset = 0 - guard let originalMessageID = data.readUUID(at: &offset), - let receiptID = data.readUUID(at: &offset) else { return nil } + guard let originalMessageID = dataCopy.readUUID(at: &offset), + let receiptID = dataCopy.readUUID(at: &offset) else { return nil } - guard offset + 8 <= data.count else { return nil } - let readerIDData = data[offset.. ChannelKeyVerifyRequest? { + // Create defensive copy + let dataCopy = Data(data) + var offset = 0 - guard let channel = data.readString(at: &offset) else { return nil } + guard let channel = dataCopy.readString(at: &offset) else { return nil } - guard offset + 8 <= data.count else { return nil } - let requesterIDData = data[offset.. ChannelKeyVerifyResponse? { + // Create defensive copy + let dataCopy = Data(data) + var offset = 0 - guard let channel = data.readString(at: &offset) else { return nil } + guard let channel = dataCopy.readString(at: &offset) else { return nil } - guard offset + 8 <= data.count else { return nil } - let responderIDData = data[offset.. ChannelPasswordUpdate? { + // Create defensive copy + let dataCopy = Data(data) + var offset = 0 - guard let channel = data.readString(at: &offset) else { return nil } + guard let channel = dataCopy.readString(at: &offset) else { return nil } - guard offset + 8 <= data.count else { return nil } - let ownerIDData = data[offset.. ChannelMetadata? { + // Create defensive copy + let dataCopy = Data(data) + var offset = 0 - guard let flags = data.readUInt8(at: &offset) else { return nil } + guard let flags = dataCopy.readUInt8(at: &offset) else { return nil } let hasKeyCommitment = (flags & 0x01) != 0 - guard let channel = data.readString(at: &offset) else { return nil } + guard let channel = dataCopy.readString(at: &offset) else { return nil } - guard offset + 8 <= data.count else { return nil } - let creatorIDData = data[offset.. NoiseIdentityAnnouncement? { - // Minimum size check - guard data.count >= 20 else { return nil } + // Create defensive copy + let dataCopy = Data(data) + + // Minimum size check: flags(1) + peerID(8) + min data lengths + guard dataCopy.count >= 20 else { return nil } var offset = 0 - guard let flags = data.readUInt8(at: &offset) else { return nil } + guard let flags = dataCopy.readUInt8(at: &offset) else { return nil } let hasPreviousPeerID = (flags & 0x01) != 0 - guard offset + 8 <= data.count else { return nil } - let peerIDData = data[offset.. VersionHello? { + // Create defensive copy + let dataCopy = Data(data) + // Minimum size check: flags(1) + versionCount(1) + at least one version(1) + preferredVersion(1) + min strings - guard data.count >= 4 else { return nil } + guard dataCopy.count >= 4 else { return nil } var offset = 0 - guard let flags = data.readUInt8(at: &offset) else { return nil } + guard let flags = dataCopy.readUInt8(at: &offset) else { return nil } let hasCapabilities = (flags & 0x01) != 0 - guard let versionCount = data.readUInt8(at: &offset) else { return nil } + guard let versionCount = dataCopy.readUInt8(at: &offset) else { return nil } var supportedVersions: [UInt8] = [] for _ in 0.. VersionAck? { + // Create defensive copy + let dataCopy = Data(data) + // Minimum size: flags(1) + version(1) + rejected(1) + min strings - guard data.count >= 5 else { return nil } + guard dataCopy.count >= 5 else { return nil } var offset = 0 - guard let flags = data.readUInt8(at: &offset) else { return nil } + guard let flags = dataCopy.readUInt8(at: &offset) else { return nil } let hasCapabilities = (flags & 0x01) != 0 let hasReason = (flags & 0x02) != 0 - guard let agreedVersion = data.readUInt8(at: &offset), - let serverVersion = data.readString(at: &offset), - let platform = data.readString(at: &offset), - let rejectedByte = data.readUInt8(at: &offset) else { return nil } + guard let agreedVersion = dataCopy.readUInt8(at: &offset), + let serverVersion = dataCopy.readString(at: &offset), + let platform = dataCopy.readString(at: &offset), + let rejectedByte = dataCopy.readUInt8(at: &offset) else { return nil } let rejected = rejectedByte != 0 var capabilities: [String]? = nil if hasCapabilities { - guard let capCount = data.readUInt8(at: &offset) else { return nil } + guard let capCount = dataCopy.readUInt8(at: &offset) else { return nil } capabilities = [] for _ in 0.. 0 + if packet.ttl > 0 { + print("🔀 Relaying handshake init packet, TTL: \(packet.ttl)") + var relayPacket = packet + relayPacket.ttl -= 1 + broadcastPacket(relayPacket) + } return } if !isPeerIDOurs(senderID) { + // Check if we already have a session (established or handshaking) + if noiseService.hasSession(with: senderID) { + print("⚠️ Received handshake init from \(senderID) but already have session/handshaking - ignoring duplicate") + return + } + // Check if we've completed version negotiation with this peer if negotiatedVersions[senderID] == nil { // Legacy peer - assume version 1 for backward compatibility @@ -2381,13 +2414,26 @@ class BluetoothMeshService: NSObject { case .noiseHandshakeResp: // Handle Noise handshake response let senderID = packet.senderID.hexEncodedString() + print("🤝 Received Noise handshake response from \(senderID)") + // Check if this handshake response is for us - if let recipientID = packet.recipientID, - !isPeerIDOurs(recipientID.hexEncodedString()) { - // Not for us, ignore - return + if let recipientID = packet.recipientID { + let recipientIDStr = recipientID.hexEncodedString() + print("🤝 Response targeted to: \(recipientIDStr), is us: \(isPeerIDOurs(recipientIDStr))") + if !isPeerIDOurs(recipientIDStr) { + // Not for us, relay if TTL > 0 + if packet.ttl > 0 { + print("🔀 Relaying handshake response packet, TTL: \(packet.ttl)") + var relayPacket = packet + relayPacket.ttl -= 1 + broadcastPacket(relayPacket) + } + return + } } + if !isPeerIDOurs(senderID) { + print("🤝 Processing handshake response from \(senderID)") handleNoiseHandshakeMessage(from: senderID, message: packet.payload, isInitiation: false) } @@ -3344,11 +3390,14 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { guard let self = self, let pendingMessages = self.pendingPrivateMessages[peerID] else { return } + print("📬 Sending \(pendingMessages.count) pending private messages to \(peerID)") + // Clear pending messages for this peer self.pendingPrivateMessages.removeValue(forKey: peerID) // Send each pending message for (content, recipientNickname, messageID) in pendingMessages { + print("📬 Sending pending message \(messageID) to \(peerID)") // Use async to avoid blocking the queue DispatchQueue.global().async { [weak self] in self?.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID) @@ -3409,10 +3458,12 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { private func handleNoiseHandshakeMessage(from peerID: String, message: Data, isInitiation: Bool) { // Use noiseService directly + print("🤝 handleNoiseHandshakeMessage from \(peerID), isInitiation: \(isInitiation), messageSize: \(message.count)") do { // Process handshake message if let response = try noiseService.processHandshakeMessage(from: peerID, message: message) { + print("🤝 Got response from processHandshakeMessage, size: \(response.count)") // Always send responses as handshake response type let packet = BitchatPacket( type: MessageType.noiseHandshakeResp.rawValue, @@ -3426,10 +3477,13 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // Use broadcastPacket instead of sendPacket to ensure it goes through the mesh broadcastPacket(packet) + } else { + print("🤝 No response needed from processHandshakeMessage") } // Check if handshake is complete if noiseService.hasEstablishedSession(with: peerID) { + print("🎉 Handshake completed with peer: \(peerID)") // Unlock rotation now that handshake is complete unlockRotation() @@ -3459,8 +3513,10 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } } catch NoiseSessionError.alreadyEstablished { // Session already established, ignore handshake + print("🤝 Handshake already established with \(peerID)") } catch { // Handshake failed + print("❌ Handshake failed with \(peerID): \(error)") } } @@ -3489,7 +3545,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { do { // Decrypt the message + print("🔓 Attempting to decrypt Noise message from \(peerID), encrypted size: \(encryptedData.count)") let decryptedData = try noiseService.decrypt(encryptedData, from: peerID) + print("🔓 Successfully decrypted message from \(peerID), decrypted size: \(decryptedData.count)") // Check if this is a special format message (type marker + payload) if decryptedData.count > 1 { @@ -3500,8 +3558,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // Extract the ACK JSON data (skip the type marker) let ackData = decryptedData.dropFirst() - // Decode the delivery ACK - if let ack = DeliveryAck.decode(from: ackData) { + // Decode the delivery ACK - try binary first, then JSON + if let ack = DeliveryAck.fromBinaryData(ackData) { + print("📨 Received binary delivery ACK via Noise: \(ack.originalMessageID) from \(ack.recipientNickname)") // Process the ACK DeliveryTracker.shared.processDeliveryAck(ack) @@ -3511,22 +3570,42 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { self.delegate?.didReceiveDeliveryAck(ack) } return + } else if let ack = DeliveryAck.decode(from: ackData) { + print("📨 Received JSON delivery ACK via Noise: \(ack.originalMessageID) from \(ack.recipientNickname)") + + // Process the ACK + DeliveryTracker.shared.processDeliveryAck(ack) + + // Notify delegate + DispatchQueue.main.async { + self.delegate?.didReceiveDeliveryAck(ack) + } + return + } else { + print("⚠️ Failed to decode delivery ACK via Noise - data size: \(ackData.count)") } } } // Try to parse as a full inner packet (for backward compatibility and other message types) if let innerPacket = BitchatPacket.from(decryptedData) { + print("📦 Successfully parsed inner packet - type: \(MessageType(rawValue: innerPacket.type)?.description ?? "unknown"), from: \(innerPacket.senderID.hexEncodedString()), to: \(innerPacket.recipientID?.hexEncodedString() ?? "broadcast")") // Process the decrypted inner packet // The packet will be handled according to its recipient ID // If it's for us, it won't be relayed handleReceivedPacket(innerPacket, from: peerID) + } else { + print("⚠️ Failed to parse inner packet from decrypted data") } } catch { // Failed to decrypt - might need to re-establish session + print("❌ Failed to decrypt Noise message from \(peerID): \(error)") if !noiseService.hasEstablishedSession(with: peerID) { + print("🔄 No Noise session with \(peerID), initiating handshake") initiateNoiseHandshake(with: peerID) + } else { + print("⚠️ Have session with \(peerID) but decryption failed") } } } @@ -3991,7 +4070,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { do { // Encrypt with Noise + print("🔐 Encrypting private message \(msgID) for \(recipientPeerID)") let encryptedData = try noiseService.encrypt(innerData, for: recipientPeerID) + print("🔐 Successfully encrypted message, size: \(encryptedData.count)") // Send as Noise encrypted message let outerPacket = BitchatPacket( @@ -4004,9 +4085,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { ttl: adaptiveTTL ) + print("📤 Broadcasting encrypted private message \(msgID) to \(recipientPeerID)") broadcastPacket(outerPacket) } catch { // Failed to encrypt message + print("❌ Failed to encrypt private message \(msgID) for \(recipientPeerID): \(error)") } } } diff --git a/bitchat/Services/DeliveryTracker.swift b/bitchat/Services/DeliveryTracker.swift index 1cf828ff..e5df9956 100644 --- a/bitchat/Services/DeliveryTracker.swift +++ b/bitchat/Services/DeliveryTracker.swift @@ -71,6 +71,8 @@ class DeliveryTracker { // Don't track broadcasts or certain message types guard message.isPrivate || message.channel != nil else { return } + print("📮 Tracking message \(message.id) - private: \(message.isPrivate), channel: \(message.channel ?? "none"), recipient: \(recipientNickname)") + let delivery = PendingDelivery( messageID: message.id, @@ -89,9 +91,21 @@ class DeliveryTracker { pendingDeliveries[message.id] = delivery pendingLock.unlock() - // Update status to sent + // Update status to sent (only if not already delivered) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.updateDeliveryStatus(message.id, status: .sent) + guard let self = self else { return } + + self.pendingLock.lock() + let stillPending = self.pendingDeliveries[message.id] != nil + self.pendingLock.unlock() + + // Only update to sent if still pending (not already delivered) + if stillPending { + print("⏱️ Updating message \(message.id) to sent status (still pending)") + self.updateDeliveryStatus(message.id, status: .sent) + } else { + print("✋ Skipping sent status update for \(message.id) - already delivered") + } } // Schedule timeout (outside of lock) @@ -102,9 +116,11 @@ class DeliveryTracker { pendingLock.lock() defer { pendingLock.unlock() } + print("✅ Processing delivery ACK for message \(ack.originalMessageID) from \(ack.recipientNickname)") // Prevent duplicate ACK processing guard !receivedAckIDs.contains(ack.ackID) else { + print("⚠️ Duplicate ACK \(ack.ackID) - ignoring") return } receivedAckIDs.insert(ack.ackID) @@ -112,6 +128,7 @@ class DeliveryTracker { // Find the pending delivery guard var delivery = pendingDeliveries[ack.originalMessageID] else { // Message might have already been delivered or timed out + print("⚠️ No pending delivery found for message \(ack.originalMessageID)") return } @@ -136,6 +153,7 @@ class DeliveryTracker { } } else { // Direct message - mark as delivered + print("💬 Marking private message \(ack.originalMessageID) as delivered to \(ack.recipientNickname)") updateDeliveryStatus(ack.originalMessageID, status: .delivered(to: ack.recipientNickname, at: Date())) pendingDeliveries.removeValue(forKey: ack.originalMessageID) } @@ -180,6 +198,7 @@ class DeliveryTracker { // MARK: - Private Methods private func updateDeliveryStatus(_ messageID: String, status: DeliveryStatus) { + print("📊 Updating delivery status for message \(messageID): \(status)") DispatchQueue.main.async { [weak self] in self?.deliveryStatusUpdated.send((messageID: messageID, status: status)) } diff --git a/bitchat/Services/NoiseEncryptionService.swift b/bitchat/Services/NoiseEncryptionService.swift index 20496623..d000a190 100644 --- a/bitchat/Services/NoiseEncryptionService.swift +++ b/bitchat/Services/NoiseEncryptionService.swift @@ -207,6 +207,11 @@ class NoiseEncryptionService { return sessionManager.getSession(for: peerID)?.isEstablished() ?? false } + /// Check if we have a session (established or handshaking) with a peer + func hasSession(with peerID: String) -> Bool { + return sessionManager.getSession(for: peerID) != nil + } + // MARK: - Encryption/Decryption /// Encrypt data for a specific peer @@ -471,11 +476,14 @@ struct NoiseMessage: Codable { } static func fromBinaryData(_ data: Data) -> NoiseMessage? { + // Create defensive copy + let dataCopy = Data(data) + var offset = 0 - guard let type = data.readUInt8(at: &offset), - let sessionID = data.readUUID(at: &offset), - let payload = data.readData(at: &offset) else { return nil } + guard let type = dataCopy.readUInt8(at: &offset), + let sessionID = dataCopy.readUUID(at: &offset), + let payload = dataCopy.readData(at: &offset) else { return nil } guard let messageType = NoiseMessageType(rawValue: type) else { return nil } diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 20f360fb..ff488922 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -3654,6 +3654,7 @@ extension ChatViewModel: BitchatDelegate { } private func updateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) { + print("🔄 Updating UI delivery status for message \(messageID): \(status)") // Helper function to check if we should skip this update func shouldSkipUpdate(currentStatus: DeliveryStatus?, newStatus: DeliveryStatus) -> Bool {