diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index 7b48f1b4..d372e4a2 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ 0245710AEAA58AD0A1425234 /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; }; + 04636BBA2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */; }; + 04636BBB2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */; }; + 04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */; }; + 04636BBD2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */; }; 04891CA92E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; }; 04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; }; 04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; }; @@ -149,6 +153,8 @@ /* Begin PBXFileReference section */ 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectedChannelTests.swift; sourceTree = ""; }; 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = ""; }; + 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryMessageHandler.swift; sourceTree = ""; }; 04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = ""; }; 04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolVersionTests.swift; sourceTree = ""; }; 04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolVersionNegotiationTests.swift; sourceTree = ""; }; @@ -320,6 +326,8 @@ ADD53BCDA233C02E53458926 /* Protocols */ = { isa = PBXGroup; children = ( + 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */, + 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */, A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */, 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */, ); @@ -566,6 +574,8 @@ 04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */, 04B6BA7A2E2166A50090FE39 /* SecurityLogger.swift in Sources */, FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */, + 04636BBA2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */, + 04636BBB2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */, 04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */, 04AD0B4F2E25B9580002A40A /* SecureIdentityStateManager.swift in Sources */, 31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */, @@ -601,6 +611,8 @@ 04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */, 04B6BA7C2E2166A50090FE39 /* SecurityLogger.swift in Sources */, 8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */, + 04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */, + 04636BBD2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */, 0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */, 1AF9F9036DEE42408D557A87 /* SecureIdentityStateManager.swift in Sources */, 7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */, diff --git a/bitchat/Protocols/BinaryEncodingUtils.swift b/bitchat/Protocols/BinaryEncodingUtils.swift new file mode 100644 index 00000000..5d7ef4ee --- /dev/null +++ b/bitchat/Protocols/BinaryEncodingUtils.swift @@ -0,0 +1,244 @@ +// +// BinaryEncodingUtils.swift +// bitchat +// +// Binary encoding utilities for efficient protocol messages +// + +import Foundation + +// MARK: - Hex Encoding/Decoding + +extension Data { + func hexEncodedString() -> String { + if self.isEmpty { + return "" + } + return self.map { String(format: "%02x", $0) }.joined() + } + + init?(hexString: String) { + let len = hexString.count / 2 + var data = Data(capacity: len) + var index = hexString.startIndex + + for _ in 0..> 8) & 0xFF)) + self.append(UInt8(value & 0xFF)) + } + + mutating func appendUInt32(_ value: UInt32) { + self.append(UInt8((value >> 24) & 0xFF)) + self.append(UInt8((value >> 16) & 0xFF)) + self.append(UInt8((value >> 8) & 0xFF)) + self.append(UInt8(value & 0xFF)) + } + + mutating func appendUInt64(_ value: UInt64) { + for i in (0..<8).reversed() { + self.append(UInt8((value >> (i * 8)) & 0xFF)) + } + } + + mutating func appendString(_ string: String, maxLength: Int = 255) { + guard let data = string.data(using: .utf8) else { return } + let length = Swift.min(data.count, maxLength) + + if maxLength <= 255 { + self.append(UInt8(length)) + } else { + self.appendUInt16(UInt16(length)) + } + + self.append(data.prefix(length)) + } + + mutating func appendData(_ data: Data, maxLength: Int = 65535) { + let length = Swift.min(data.count, maxLength) + + if maxLength <= 255 { + self.append(UInt8(length)) + } else { + self.appendUInt16(UInt16(length)) + } + + self.append(data.prefix(length)) + } + + mutating func appendDate(_ date: Date) { + let timestamp = UInt64(date.timeIntervalSince1970 * 1000) // milliseconds + self.appendUInt64(timestamp) + } + + mutating func appendUUID(_ uuid: String) { + // Convert UUID string to 16 bytes + var uuidData = Data(count: 16) + + let cleanUUID = uuid.replacingOccurrences(of: "-", with: "") + var index = cleanUUID.startIndex + + for i in 0..<16 { + guard index < cleanUUID.endIndex else { break } + let nextIndex = cleanUUID.index(index, offsetBy: 2) + if let byte = UInt8(String(cleanUUID[index.. UInt8? { + guard offset >= 0 && offset < self.count else { return nil } + let value = self[offset] + offset += 1 + return value + } + + func readUInt16(at offset: inout Int) -> UInt16? { + guard offset + 2 <= self.count else { return nil } + let value = UInt16(self[offset]) << 8 | UInt16(self[offset + 1]) + offset += 2 + return value + } + + func readUInt32(at offset: inout Int) -> UInt32? { + guard offset + 4 <= self.count else { return nil } + let value = UInt32(self[offset]) << 24 | + UInt32(self[offset + 1]) << 16 | + UInt32(self[offset + 2]) << 8 | + UInt32(self[offset + 3]) + offset += 4 + return value + } + + func readUInt64(at offset: inout Int) -> UInt64? { + guard offset + 8 <= self.count else { return nil } + var value: UInt64 = 0 + for i in 0..<8 { + value = (value << 8) | UInt64(self[offset + i]) + } + offset += 8 + return value + } + + func readString(at offset: inout Int, maxLength: Int = 255) -> String? { + let length: Int + + if maxLength <= 255 { + guard let len = readUInt8(at: &offset) else { return nil } + length = Int(len) + } else { + guard let len = readUInt16(at: &offset) else { return nil } + length = Int(len) + } + + guard offset + length <= self.count else { return nil } + + let stringData = self[offset.. Data? { + let length: Int + + if maxLength <= 255 { + guard let len = readUInt8(at: &offset) else { return nil } + length = Int(len) + } else { + guard let len = readUInt16(at: &offset) else { return nil } + length = Int(len) + } + + guard offset + length <= self.count else { return nil } + + let data = self[offset.. Date? { + guard let timestamp = readUInt64(at: &offset) else { return nil } + return Date(timeIntervalSince1970: Double(timestamp) / 1000.0) + } + + func readUUID(at offset: inout Int) -> String? { + guard offset + 16 <= self.count else { return nil } + + let uuidData = self[offset.. Data? { + guard offset + count <= self.count else { return nil } + + let data = self[offset.. Data + static func fromBinaryData(_ data: Data) -> Self? +} + +// MARK: - Message Type Registry + +enum BinaryMessageType: UInt8 { + case deliveryAck = 0x01 + case readReceipt = 0x02 + case channelKeyVerifyRequest = 0x03 + case channelKeyVerifyResponse = 0x04 + case channelPasswordUpdate = 0x05 + case channelMetadata = 0x06 + case versionHello = 0x07 + case versionAck = 0x08 + case noiseIdentityAnnouncement = 0x09 + case noiseMessage = 0x0A +} \ No newline at end of file diff --git a/bitchat/Protocols/BinaryMessageHandler.swift b/bitchat/Protocols/BinaryMessageHandler.swift new file mode 100644 index 00000000..8c5241fa --- /dev/null +++ b/bitchat/Protocols/BinaryMessageHandler.swift @@ -0,0 +1,124 @@ +// +// BinaryMessageHandler.swift +// bitchat +// +// Unified binary message encoding/decoding handler +// + +import Foundation + +struct BinaryMessageHandler { + + // MARK: - Encoding + + static func encode(message: Any, type: MessageType) -> Data? { + switch type { + case .deliveryAck: + return (message as? DeliveryAck)?.toBinaryData() + case .readReceipt: + return (message as? ReadReceipt)?.toBinaryData() + case .channelKeyVerifyRequest: + return (message as? ChannelKeyVerifyRequest)?.toBinaryData() + case .channelKeyVerifyResponse: + return (message as? ChannelKeyVerifyResponse)?.toBinaryData() + case .channelPasswordUpdate: + return (message as? ChannelPasswordUpdate)?.toBinaryData() + case .channelMetadata: + return (message as? ChannelMetadata)?.toBinaryData() + case .versionHello: + return (message as? VersionHello)?.toBinaryData() + case .versionAck: + return (message as? VersionAck)?.toBinaryData() + case .noiseIdentityAnnounce: + return (message as? NoiseIdentityAnnouncement)?.toBinaryData() + case .noiseHandshakeInit, .noiseHandshakeResp: + // Noise handshake messages are already binary + return message as? Data + case .noiseEncrypted: + return (message as? NoiseMessage)?.toBinaryData() + default: + return nil + } + } + + // MARK: - Decoding + + static func decode(data: Data, type: MessageType) -> Any? { + switch type { + case .deliveryAck: + return DeliveryAck.fromBinaryData(data) + case .readReceipt: + return ReadReceipt.fromBinaryData(data) + case .channelKeyVerifyRequest: + return ChannelKeyVerifyRequest.fromBinaryData(data) + case .channelKeyVerifyResponse: + return ChannelKeyVerifyResponse.fromBinaryData(data) + case .channelPasswordUpdate: + return ChannelPasswordUpdate.fromBinaryData(data) + case .channelMetadata: + return ChannelMetadata.fromBinaryData(data) + case .versionHello: + return VersionHello.fromBinaryData(data) + case .versionAck: + return VersionAck.fromBinaryData(data) + case .noiseIdentityAnnounce: + return NoiseIdentityAnnouncement.fromBinaryData(data) + case .noiseHandshakeInit, .noiseHandshakeResp: + // Noise handshake messages are already binary + return data + case .noiseEncrypted: + return NoiseMessage.fromBinaryData(data) + default: + return nil + } + } + + // MARK: - Legacy JSON Support (for migration) + + static func decodeJSON(data: Data, type: MessageType) -> Any? { + switch type { + case .deliveryAck: + return DeliveryAck.decode(from: data) + case .readReceipt: + return ReadReceipt.decode(from: data) + case .channelKeyVerifyRequest: + return ChannelKeyVerifyRequest.decode(from: data) + case .channelKeyVerifyResponse: + return ChannelKeyVerifyResponse.decode(from: data) + case .channelPasswordUpdate: + return ChannelPasswordUpdate.decode(from: data) + case .channelMetadata: + return ChannelMetadata.decode(from: data) + case .versionHello: + return VersionHello.decode(from: data) + case .versionAck: + return VersionAck.decode(from: data) + case .noiseIdentityAnnounce: + return NoiseIdentityAnnouncement.decode(from: data) + case .noiseEncrypted: + return NoiseMessage.decode(from: data) + default: + return nil + } + } + + // MARK: - Format Detection + + static func isBinaryFormat(_ data: Data) -> Bool { + // Simple heuristic: JSON always starts with { or [ + guard let firstByte = data.first else { return false } + return firstByte != 0x7B && firstByte != 0x5B // { and [ + } + + // MARK: - Unified Decode (with fallback) + + static func decodeWithFallback(data: Data, type: MessageType) -> Any? { + // Try binary first + if let result = decode(data: data, type: type) { + return result + } + + // Fallback to JSON for backward compatibility + return decodeJSON(data: data, type: type) + } +} \ No newline at end of file diff --git a/bitchat/Protocols/BitchatProtocol.swift b/bitchat/Protocols/BitchatProtocol.swift index ec421a3b..8ba10368 100644 --- a/bitchat/Protocols/BitchatProtocol.swift +++ b/bitchat/Protocols/BitchatProtocol.swift @@ -206,6 +206,16 @@ struct DeliveryAck: Codable { self.hopCount = hopCount } + // For binary decoding + private init(originalMessageID: String, ackID: String, recipientID: String, recipientNickname: String, timestamp: Date, hopCount: UInt8) { + self.originalMessageID = originalMessageID + self.ackID = ackID + self.recipientID = recipientID + self.recipientNickname = recipientNickname + self.timestamp = timestamp + self.hopCount = hopCount + } + func encode() -> Data? { try? JSONEncoder().encode(self) } @@ -213,6 +223,59 @@ struct DeliveryAck: Codable { static func decode(from data: Data) -> DeliveryAck? { try? JSONDecoder().decode(DeliveryAck.self, from: data) } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + data.appendUUID(originalMessageID) + data.appendUUID(ackID) + // RecipientID as 8-byte hex string + var recipientData = Data() + var tempID = recipientID + while tempID.count >= 2 && recipientData.count < 8 { + let hexByte = String(tempID.prefix(2)) + if let byte = UInt8(hexByte, radix: 16) { + recipientData.append(byte) + } + tempID = String(tempID.dropFirst(2)) + } + while recipientData.count < 8 { + recipientData.append(0) + } + data.append(recipientData) + data.appendUInt8(hopCount) + data.appendDate(timestamp) + data.appendString(recipientNickname) + return data + } + + static func fromBinaryData(_ data: Data) -> DeliveryAck? { + // Create defensive copy + let dataCopy = Data(data) + + // Minimum size: 2 UUIDs (32) + recipientID (8) + hopCount (1) + timestamp (8) + min nickname + guard dataCopy.count >= 50 else { return nil } + + var offset = 0 + + guard let originalMessageID = dataCopy.readUUID(at: &offset), + let ackID = dataCopy.readUUID(at: &offset) else { return nil } + + guard let recipientIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil } + let recipientID = recipientIDData.hexEncodedString() + + guard let hopCount = dataCopy.readUInt8(at: &offset), + let timestamp = dataCopy.readDate(at: &offset), + let recipientNickname = dataCopy.readString(at: &offset) else { return nil } + + return DeliveryAck(originalMessageID: originalMessageID, + ackID: ackID, + recipientID: recipientID, + recipientNickname: recipientNickname, + timestamp: timestamp, + hopCount: hopCount) + } } // Read receipt structure @@ -231,6 +294,15 @@ struct ReadReceipt: Codable { self.timestamp = Date() } + // For binary decoding + private init(originalMessageID: String, receiptID: String, readerID: String, readerNickname: String, timestamp: Date) { + self.originalMessageID = originalMessageID + self.receiptID = receiptID + self.readerID = readerID + self.readerNickname = readerNickname + self.timestamp = timestamp + } + func encode() -> Data? { try? JSONEncoder().encode(self) } @@ -238,6 +310,56 @@ struct ReadReceipt: Codable { static func decode(from data: Data) -> ReadReceipt? { try? JSONDecoder().decode(ReadReceipt.self, from: data) } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + data.appendUUID(originalMessageID) + data.appendUUID(receiptID) + // ReaderID as 8-byte hex string + var readerData = Data() + var tempID = readerID + while tempID.count >= 2 && readerData.count < 8 { + let hexByte = String(tempID.prefix(2)) + if let byte = UInt8(hexByte, radix: 16) { + readerData.append(byte) + } + tempID = String(tempID.dropFirst(2)) + } + while readerData.count < 8 { + readerData.append(0) + } + data.append(readerData) + data.appendDate(timestamp) + data.appendString(readerNickname) + return data + } + + static func fromBinaryData(_ data: Data) -> ReadReceipt? { + // Create defensive copy + let dataCopy = Data(data) + + // Minimum size: 2 UUIDs (32) + readerID (8) + timestamp (8) + min nickname + guard dataCopy.count >= 49 else { return nil } + + var offset = 0 + + guard let originalMessageID = dataCopy.readUUID(at: &offset), + let receiptID = dataCopy.readUUID(at: &offset) else { return nil } + + guard let readerIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil } + let readerID = readerIDData.hexEncodedString() + + guard let timestamp = dataCopy.readDate(at: &offset), + let readerNickname = dataCopy.readString(at: &offset) else { return nil } + + return ReadReceipt(originalMessageID: originalMessageID, + receiptID: receiptID, + readerID: readerID, + readerNickname: readerNickname, + timestamp: timestamp) + } } // Channel key verification request @@ -254,6 +376,14 @@ struct ChannelKeyVerifyRequest: Codable { self.timestamp = Date() } + // For binary decoding + private init(channel: String, requesterID: String, keyCommitment: String, timestamp: Date) { + self.channel = channel + self.requesterID = requesterID + self.keyCommitment = keyCommitment + self.timestamp = timestamp + } + func encode() -> Data? { return try? JSONEncoder().encode(self) } @@ -261,6 +391,50 @@ struct ChannelKeyVerifyRequest: Codable { static func decode(from data: Data) -> ChannelKeyVerifyRequest? { try? JSONDecoder().decode(ChannelKeyVerifyRequest.self, from: data) } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + data.appendString(channel) + // RequesterID as 8-byte hex string + var requesterData = Data() + var tempID = requesterID + while tempID.count >= 2 && requesterData.count < 8 { + let hexByte = String(tempID.prefix(2)) + if let byte = UInt8(hexByte, radix: 16) { + requesterData.append(byte) + } + tempID = String(tempID.dropFirst(2)) + } + while requesterData.count < 8 { + requesterData.append(0) + } + data.append(requesterData) + data.appendString(keyCommitment) + data.appendDate(timestamp) + return data + } + + static func fromBinaryData(_ data: Data) -> ChannelKeyVerifyRequest? { + // Create defensive copy + let dataCopy = Data(data) + + var offset = 0 + + guard let channel = dataCopy.readString(at: &offset) else { return nil } + + guard let requesterIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil } + let requesterID = requesterIDData.hexEncodedString() + + guard let keyCommitment = dataCopy.readString(at: &offset), + let timestamp = dataCopy.readDate(at: &offset) else { return nil } + + return ChannelKeyVerifyRequest(channel: channel, + requesterID: requesterID, + keyCommitment: keyCommitment, + timestamp: timestamp) + } } // Channel key verification response @@ -277,6 +451,14 @@ struct ChannelKeyVerifyResponse: Codable { self.timestamp = Date() } + // For binary decoding + private init(channel: String, responderID: String, verified: Bool, timestamp: Date) { + self.channel = channel + self.responderID = responderID + self.verified = verified + self.timestamp = timestamp + } + func encode() -> Data? { return try? JSONEncoder().encode(self) } @@ -284,6 +466,52 @@ struct ChannelKeyVerifyResponse: Codable { static func decode(from data: Data) -> ChannelKeyVerifyResponse? { try? JSONDecoder().decode(ChannelKeyVerifyResponse.self, from: data) } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + data.appendString(channel) + // ResponderID as 8-byte hex string + var responderData = Data() + var tempID = responderID + while tempID.count >= 2 && responderData.count < 8 { + let hexByte = String(tempID.prefix(2)) + if let byte = UInt8(hexByte, radix: 16) { + responderData.append(byte) + } + tempID = String(tempID.dropFirst(2)) + } + while responderData.count < 8 { + responderData.append(0) + } + data.append(responderData) + data.appendUInt8(verified ? 1 : 0) + data.appendDate(timestamp) + return data + } + + static func fromBinaryData(_ data: Data) -> ChannelKeyVerifyResponse? { + // Create defensive copy + let dataCopy = Data(data) + + var offset = 0 + + guard let channel = dataCopy.readString(at: &offset) else { return nil } + + guard let responderIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil } + let responderID = responderIDData.hexEncodedString() + + guard let verifiedByte = dataCopy.readUInt8(at: &offset), + let timestamp = dataCopy.readDate(at: &offset) else { return nil } + + let verified = verifiedByte != 0 + + return ChannelKeyVerifyResponse(channel: channel, + responderID: responderID, + verified: verified, + timestamp: timestamp) + } } // Channel password update (sent by owner to members) @@ -304,6 +532,16 @@ struct ChannelPasswordUpdate: Codable { self.timestamp = Date() } + // For binary decoding + private init(channel: String, ownerID: String, ownerFingerprint: String, encryptedPassword: Data, newKeyCommitment: String, timestamp: Date) { + self.channel = channel + self.ownerID = ownerID + self.ownerFingerprint = ownerFingerprint + self.encryptedPassword = encryptedPassword + self.newKeyCommitment = newKeyCommitment + self.timestamp = timestamp + } + func encode() -> Data? { return try? JSONEncoder().encode(self) } @@ -311,6 +549,56 @@ struct ChannelPasswordUpdate: Codable { static func decode(from data: Data) -> ChannelPasswordUpdate? { try? JSONDecoder().decode(ChannelPasswordUpdate.self, from: data) } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + data.appendString(channel) + // OwnerID as 8-byte hex string + var ownerData = Data() + var tempID = ownerID + while tempID.count >= 2 && ownerData.count < 8 { + let hexByte = String(tempID.prefix(2)) + if let byte = UInt8(hexByte, radix: 16) { + ownerData.append(byte) + } + tempID = String(tempID.dropFirst(2)) + } + while ownerData.count < 8 { + ownerData.append(0) + } + data.append(ownerData) + data.appendString(ownerFingerprint) + data.appendData(encryptedPassword) + data.appendString(newKeyCommitment) + data.appendDate(timestamp) + return data + } + + static func fromBinaryData(_ data: Data) -> ChannelPasswordUpdate? { + // Create defensive copy + let dataCopy = Data(data) + + var offset = 0 + + guard let channel = dataCopy.readString(at: &offset) else { return nil } + + guard let ownerIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil } + let ownerID = ownerIDData.hexEncodedString() + + guard let ownerFingerprint = dataCopy.readString(at: &offset), + let encryptedPassword = dataCopy.readData(at: &offset), + let newKeyCommitment = dataCopy.readString(at: &offset), + let timestamp = dataCopy.readDate(at: &offset) else { return nil } + + return ChannelPasswordUpdate(channel: channel, + ownerID: ownerID, + ownerFingerprint: ownerFingerprint, + encryptedPassword: encryptedPassword, + newKeyCommitment: newKeyCommitment, + timestamp: timestamp) + } } // Channel metadata announcement @@ -331,6 +619,16 @@ struct ChannelMetadata: Codable { self.keyCommitment = keyCommitment } + // For binary decoding + private init(channel: String, creatorID: String, creatorFingerprint: String, createdAt: Date, isPasswordProtected: Bool, keyCommitment: String?) { + self.channel = channel + self.creatorID = creatorID + self.creatorFingerprint = creatorFingerprint + self.createdAt = createdAt + self.isPasswordProtected = isPasswordProtected + self.keyCommitment = keyCommitment + } + func encode() -> Data? { return try? JSONEncoder().encode(self) } @@ -338,6 +636,75 @@ struct ChannelMetadata: Codable { static func decode(from data: Data) -> ChannelMetadata? { try? JSONDecoder().decode(ChannelMetadata.self, from: data) } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + + // Flags byte: bit 0 = hasKeyCommitment + var flags: UInt8 = 0 + if keyCommitment != nil { flags |= 0x01 } + data.appendUInt8(flags) + + data.appendString(channel) + // CreatorID as 8-byte hex string + var creatorData = Data() + var tempID = creatorID + while tempID.count >= 2 && creatorData.count < 8 { + let hexByte = String(tempID.prefix(2)) + if let byte = UInt8(hexByte, radix: 16) { + creatorData.append(byte) + } + tempID = String(tempID.dropFirst(2)) + } + while creatorData.count < 8 { + creatorData.append(0) + } + data.append(creatorData) + data.appendString(creatorFingerprint) + data.appendDate(createdAt) + data.appendUInt8(isPasswordProtected ? 1 : 0) + + if let keyCommitment = keyCommitment { + data.appendString(keyCommitment) + } + + return data + } + + static func fromBinaryData(_ data: Data) -> ChannelMetadata? { + // Create defensive copy + let dataCopy = Data(data) + + var offset = 0 + + guard let flags = dataCopy.readUInt8(at: &offset) else { return nil } + let hasKeyCommitment = (flags & 0x01) != 0 + + guard let channel = dataCopy.readString(at: &offset) else { return nil } + + guard let creatorIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil } + let creatorID = creatorIDData.hexEncodedString() + + guard let creatorFingerprint = dataCopy.readString(at: &offset), + let createdAt = dataCopy.readDate(at: &offset), + let isPasswordProtectedByte = dataCopy.readUInt8(at: &offset) else { return nil } + + let isPasswordProtected = isPasswordProtectedByte != 0 + + var keyCommitment: String? = nil + if hasKeyCommitment { + keyCommitment = dataCopy.readString(at: &offset) + } + + return ChannelMetadata(channel: channel, + creatorID: creatorID, + creatorFingerprint: creatorFingerprint, + createdAt: createdAt, + isPasswordProtected: isPasswordProtected, + keyCommitment: keyCommitment) + } } // MARK: - Peer Identity Rotation @@ -369,6 +736,97 @@ struct NoiseIdentityAnnouncement: Codable { static func decode(from data: Data) -> NoiseIdentityAnnouncement? { return try? JSONDecoder().decode(NoiseIdentityAnnouncement.self, from: data) } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + + // Flags byte: bit 0 = hasPreviousPeerID + var flags: UInt8 = 0 + if previousPeerID != nil { flags |= 0x01 } + data.appendUInt8(flags) + + // PeerID as 8-byte hex string + var peerData = Data() + var tempID = peerID + while tempID.count >= 2 && peerData.count < 8 { + let hexByte = String(tempID.prefix(2)) + if let byte = UInt8(hexByte, radix: 16) { + peerData.append(byte) + } + tempID = String(tempID.dropFirst(2)) + } + while peerData.count < 8 { + peerData.append(0) + } + data.append(peerData) + + data.appendData(publicKey) + data.appendData(signingPublicKey) + data.appendString(nickname) + data.appendDate(timestamp) + + if let previousPeerID = previousPeerID { + // Previous PeerID as 8-byte hex string + var prevData = Data() + var tempPrevID = previousPeerID + while tempPrevID.count >= 2 && prevData.count < 8 { + let hexByte = String(tempPrevID.prefix(2)) + if let byte = UInt8(hexByte, radix: 16) { + prevData.append(byte) + } + tempPrevID = String(tempPrevID.dropFirst(2)) + } + while prevData.count < 8 { + prevData.append(0) + } + data.append(prevData) + } + + data.appendData(signature) + + return data + } + + static func fromBinaryData(_ data: Data) -> NoiseIdentityAnnouncement? { + // 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 = dataCopy.readUInt8(at: &offset) else { return nil } + let hasPreviousPeerID = (flags & 0x01) != 0 + + // Read peerID using safe method + guard let peerIDBytes = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil } + let peerID = peerIDBytes.hexEncodedString() + + guard let publicKey = dataCopy.readData(at: &offset), + let signingPublicKey = dataCopy.readData(at: &offset), + let nickname = dataCopy.readString(at: &offset), + let timestamp = dataCopy.readDate(at: &offset) else { return nil } + + var previousPeerID: String? = nil + if hasPreviousPeerID { + // Read previousPeerID using safe method + guard let prevIDBytes = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil } + previousPeerID = prevIDBytes.hexEncodedString() + } + + guard let signature = dataCopy.readData(at: &offset) else { return nil } + + return NoiseIdentityAnnouncement(peerID: peerID, + publicKey: publicKey, + signingPublicKey: signingPublicKey, + nickname: nickname, + timestamp: timestamp, + previousPeerID: previousPeerID, + signature: signature) + } } // Binding between ephemeral peer ID and cryptographic identity @@ -447,6 +905,76 @@ struct VersionHello: Codable { static func decode(from data: Data) -> VersionHello? { try? JSONDecoder().decode(VersionHello.self, from: data) } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + + // Flags byte: bit 0 = hasCapabilities + var flags: UInt8 = 0 + if capabilities != nil { flags |= 0x01 } + data.appendUInt8(flags) + + // Supported versions array + data.appendUInt8(UInt8(supportedVersions.count)) + for version in supportedVersions { + data.appendUInt8(version) + } + + data.appendUInt8(preferredVersion) + data.appendString(clientVersion) + data.appendString(platform) + + if let capabilities = capabilities { + data.appendUInt8(UInt8(capabilities.count)) + for capability in capabilities { + data.appendString(capability) + } + } + + return data + } + + static func fromBinaryData(_ data: Data) -> 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 dataCopy.count >= 4 else { return nil } + + var offset = 0 + + guard let flags = dataCopy.readUInt8(at: &offset) else { return nil } + let hasCapabilities = (flags & 0x01) != 0 + + guard let versionCount = dataCopy.readUInt8(at: &offset) else { return nil } + var supportedVersions: [UInt8] = [] + for _ in 0.. VersionAck? { try? JSONDecoder().decode(VersionAck.self, from: data) } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + + // Flags byte: bit 0 = hasCapabilities, bit 1 = hasReason + var flags: UInt8 = 0 + if capabilities != nil { flags |= 0x01 } + if reason != nil { flags |= 0x02 } + data.appendUInt8(flags) + + data.appendUInt8(agreedVersion) + data.appendString(serverVersion) + data.appendString(platform) + data.appendUInt8(rejected ? 1 : 0) + + if let capabilities = capabilities { + data.appendUInt8(UInt8(capabilities.count)) + for capability in capabilities { + data.appendString(capability) + } + } + + if let reason = reason { + data.appendString(reason) + } + + return data + } + + static func fromBinaryData(_ data: Data) -> VersionAck? { + // Create defensive copy + let dataCopy = Data(data) + + // Minimum size: flags(1) + version(1) + rejected(1) + min strings + guard dataCopy.count >= 5 else { return nil } + + var offset = 0 + + guard let flags = dataCopy.readUInt8(at: &offset) else { return nil } + let hasCapabilities = (flags & 0x01) != 0 + let hasReason = (flags & 0x02) != 0 + + 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 = dataCopy.readUInt8(at: &offset) else { return nil } + capabilities = [] + for _ in 0.. String { - if self.isEmpty { - return "" - } - return self.map { String(format: "%02x", $0) }.joined() - } - - init?(hexString: String) { - let len = hexString.count / 2 - var data = Data(capacity: len) - var index = hexString.startIndex - - 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 @@ -2384,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) } @@ -3347,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) @@ -3412,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, @@ -3429,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() @@ -3462,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)") } } @@ -3492,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 { @@ -3503,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) @@ -3514,28 +3570,48 @@ 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") } } } private func handleChannelKeyVerifyRequest(from peerID: String, data: Data) { - guard let request = ChannelKeyVerifyRequest.decode(from: data) else { return } + guard let request = ChannelKeyVerifyRequest.fromBinaryData(data) ?? ChannelKeyVerifyRequest.decode(from: data) else { return } // Forward to delegate (ChatViewModel) to handle DispatchQueue.main.async { [weak self] in @@ -3544,7 +3620,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } private func handleChannelKeyVerifyResponse(from peerID: String, data: Data) { - guard let response = ChannelKeyVerifyResponse.decode(from: data) else { return } + guard let response = ChannelKeyVerifyResponse.fromBinaryData(data) ?? ChannelKeyVerifyResponse.decode(from: data) else { return } // Forward to delegate (ChatViewModel) to handle DispatchQueue.main.async { [weak self] in @@ -3561,7 +3637,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { let decryptedData = try noiseService.decrypt(data, from: peerID) // Parse the password update - guard let update = ChannelPasswordUpdate.decode(from: decryptedData) else { return } + guard let update = ChannelPasswordUpdate.fromBinaryData(decryptedData) ?? ChannelPasswordUpdate.decode(from: decryptedData) else { return } // Forward to delegate (ChatViewModel) to handle DispatchQueue.main.async { [weak self] in @@ -3573,7 +3649,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { private func handleChannelMetadata(from peerID: String, data: Data) { // Channel metadata is broadcast unencrypted (like channel announcements) - guard let metadata = ChannelMetadata.decode(from: data) else { return } + guard let metadata = ChannelMetadata.fromBinaryData(data) ?? ChannelMetadata.decode(from: data) else { return } // Forward to delegate (ChatViewModel) to handle DispatchQueue.main.async { [weak self] in @@ -3584,7 +3660,26 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // MARK: - Protocol Version Negotiation private func handleVersionHello(from peerID: String, data: Data, peripheral: CBPeripheral? = nil) { - guard let hello = VersionHello.decode(from: data) else { + // Create a copy to avoid potential race conditions + let dataCopy = Data(data) + + // Safety check for empty data + guard !dataCopy.isEmpty else { + SecurityLogger.log("Received empty version hello data from \(peerID)", category: SecurityLogger.session, level: .error) + return + } + + // Try JSON first if it looks like JSON + let hello: VersionHello? + if let firstByte = dataCopy.first, firstByte == 0x7B { // '{' character + SecurityLogger.log("Version hello from \(peerID) appears to be JSON (size: \(dataCopy.count))", category: SecurityLogger.session, level: .debug) + hello = VersionHello.decode(from: dataCopy) ?? VersionHello.fromBinaryData(dataCopy) + } else { + SecurityLogger.log("Version hello from \(peerID) appears to be binary (size: \(dataCopy.count), first byte: \(dataCopy.first?.description ?? "nil"))", category: SecurityLogger.session, level: .debug) + hello = VersionHello.fromBinaryData(dataCopy) ?? VersionHello.decode(from: dataCopy) + } + + guard let hello = hello else { SecurityLogger.log("Failed to decode version hello from \(peerID)", category: SecurityLogger.session, level: .error) return } @@ -3636,7 +3731,24 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } private func handleVersionAck(from peerID: String, data: Data) { - guard let ack = VersionAck.decode(from: data) else { + // Create a copy to avoid potential race conditions + let dataCopy = Data(data) + + // Safety check for empty data + guard !dataCopy.isEmpty else { + SecurityLogger.log("Received empty version ack data from \(peerID)", category: SecurityLogger.session, level: .error) + return + } + + // Try JSON first if it looks like JSON + let ack: VersionAck? + if let firstByte = dataCopy.first, firstByte == 0x7B { // '{' character + ack = VersionAck.decode(from: dataCopy) ?? VersionAck.fromBinaryData(dataCopy) + } else { + ack = VersionAck.fromBinaryData(dataCopy) ?? VersionAck.decode(from: dataCopy) + } + + guard let ack = ack else { SecurityLogger.log("Failed to decode version ack from \(peerID)", category: SecurityLogger.session, level: .error) return } @@ -3678,7 +3790,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { platform: getPlatformString() ) - guard let helloData = hello.encode() else { return } + let helloData = hello.toBinaryData() let packet = BitchatPacket( type: MessageType.versionHello.rawValue, @@ -3704,7 +3816,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } private func sendVersionAck(_ ack: VersionAck, to peerID: String) { - guard let ackData = ack.encode() else { return } + let ackData = ack.toBinaryData() let packet = BitchatPacket( type: MessageType.versionAck.rawValue, @@ -3730,7 +3842,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } func sendChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, to peers: [String]) { - guard let requestData = request.encode() else { return } + let requestData = request.toBinaryData() // Send to each peer for peerID in peers { @@ -3749,7 +3861,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } func sendChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, to peerID: String) { - guard let responseData = response.encode() else { return } + let responseData = response.toBinaryData() let packet = BitchatPacket( type: MessageType.channelKeyVerifyResponse.rawValue, @@ -3784,7 +3896,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { newKeyCommitment: newCommitment ) - guard let updateData = update.encode() else { return } + let updateData = update.toBinaryData() do { // Encrypt the entire update message @@ -3852,9 +3964,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { ) // Encode the announcement - guard let announcementData = announcement.encode() else { - return - } + let announcementData = announcement.toBinaryData() let packet = BitchatPacket( type: MessageType.noiseIdentityAnnounce.rawValue, @@ -3960,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( @@ -3973,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 046a883f..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 @@ -459,6 +464,31 @@ struct NoiseMessage: Codable { return nil } } + + // MARK: - Binary Encoding + + func toBinaryData() -> Data { + var data = Data() + data.appendUInt8(type) + data.appendUUID(sessionID) + data.appendData(payload) + return data + } + + static func fromBinaryData(_ data: Data) -> NoiseMessage? { + // Create defensive copy + let dataCopy = Data(data) + + var offset = 0 + + 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 } + + return NoiseMessage(type: messageType, sessionID: sessionID, payload: payload) + } } // MARK: - Errors 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 {