mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 18:25:19 +00:00
Merge pull request #292 from permissionlesstech/binary-protocol-migration
Fix Noise handshake failures and implement binary protocol migration
This commit is contained in:
@@ -8,6 +8,10 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0245710AEAA58AD0A1425234 /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; };
|
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 */; };
|
04891CA92E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
||||||
04891CAA2E22971E0064A111 /* 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 */; };
|
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
|
||||||
@@ -149,6 +153,8 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectedChannelTests.swift; sourceTree = "<group>"; };
|
036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectedChannelTests.swift; sourceTree = "<group>"; };
|
||||||
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 = "<group>"; };
|
||||||
|
04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryMessageHandler.swift; sourceTree = "<group>"; };
|
||||||
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
||||||
04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolVersionTests.swift; sourceTree = "<group>"; };
|
04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolVersionTests.swift; sourceTree = "<group>"; };
|
||||||
04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolVersionNegotiationTests.swift; sourceTree = "<group>"; };
|
04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolVersionNegotiationTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -320,6 +326,8 @@
|
|||||||
ADD53BCDA233C02E53458926 /* Protocols */ = {
|
ADD53BCDA233C02E53458926 /* Protocols */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */,
|
||||||
|
04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */,
|
||||||
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */,
|
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */,
|
||||||
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */,
|
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */,
|
||||||
);
|
);
|
||||||
@@ -566,6 +574,8 @@
|
|||||||
04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
|
04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
|
||||||
04B6BA7A2E2166A50090FE39 /* SecurityLogger.swift in Sources */,
|
04B6BA7A2E2166A50090FE39 /* SecurityLogger.swift in Sources */,
|
||||||
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
|
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
|
||||||
|
04636BBA2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
|
||||||
|
04636BBB2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */,
|
||||||
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */,
|
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */,
|
||||||
04AD0B4F2E25B9580002A40A /* SecureIdentityStateManager.swift in Sources */,
|
04AD0B4F2E25B9580002A40A /* SecureIdentityStateManager.swift in Sources */,
|
||||||
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */,
|
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */,
|
||||||
@@ -601,6 +611,8 @@
|
|||||||
04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
|
04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
|
||||||
04B6BA7C2E2166A50090FE39 /* SecurityLogger.swift in Sources */,
|
04B6BA7C2E2166A50090FE39 /* SecurityLogger.swift in Sources */,
|
||||||
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
|
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
|
||||||
|
04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
|
||||||
|
04636BBD2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */,
|
||||||
0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */,
|
0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */,
|
||||||
1AF9F9036DEE42408D557A87 /* SecureIdentityStateManager.swift in Sources */,
|
1AF9F9036DEE42408D557A87 /* SecureIdentityStateManager.swift in Sources */,
|
||||||
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */,
|
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */,
|
||||||
|
|||||||
@@ -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..<len {
|
||||||
|
let nextIndex = hexString.index(index, offsetBy: 2)
|
||||||
|
guard let byte = UInt8(String(hexString[index..<nextIndex]), radix: 16) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data.append(byte)
|
||||||
|
index = nextIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
self = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Binary Encoding Utilities
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
// MARK: Writing
|
||||||
|
|
||||||
|
mutating func appendUInt8(_ value: UInt8) {
|
||||||
|
self.append(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func appendUInt16(_ value: UInt16) {
|
||||||
|
self.append(UInt8((value >> 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..<nextIndex]), radix: 16) {
|
||||||
|
uuidData[i] = byte
|
||||||
|
}
|
||||||
|
index = nextIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
self.append(uuidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Reading
|
||||||
|
|
||||||
|
func readUInt8(at offset: inout Int) -> 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..<offset + length]
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
return String(data: stringData, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readData(at offset: inout Int, maxLength: Int = 65535) -> 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..<offset + length]
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDate(at offset: inout Int) -> 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..<offset + 16]
|
||||||
|
offset += 16
|
||||||
|
|
||||||
|
// Convert 16 bytes to UUID string format
|
||||||
|
let uuid = uuidData.map { String(format: "%02x", $0) }.joined()
|
||||||
|
|
||||||
|
// Insert hyphens at proper positions: 8-4-4-4-12
|
||||||
|
var result = ""
|
||||||
|
for (index, char) in uuid.enumerated() {
|
||||||
|
if index == 8 || index == 12 || index == 16 || index == 20 {
|
||||||
|
result += "-"
|
||||||
|
}
|
||||||
|
result.append(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFixedBytes(at offset: inout Int, count: Int) -> Data? {
|
||||||
|
guard offset + count <= self.count else { return nil }
|
||||||
|
|
||||||
|
let data = self[offset..<offset + count]
|
||||||
|
offset += count
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Binary Message Protocol
|
||||||
|
|
||||||
|
protocol BinaryEncodable {
|
||||||
|
func toBinaryData() -> 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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -206,6 +206,16 @@ struct DeliveryAck: Codable {
|
|||||||
self.hopCount = hopCount
|
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? {
|
func encode() -> Data? {
|
||||||
try? JSONEncoder().encode(self)
|
try? JSONEncoder().encode(self)
|
||||||
}
|
}
|
||||||
@@ -213,6 +223,59 @@ struct DeliveryAck: Codable {
|
|||||||
static func decode(from data: Data) -> DeliveryAck? {
|
static func decode(from data: Data) -> DeliveryAck? {
|
||||||
try? JSONDecoder().decode(DeliveryAck.self, from: data)
|
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
|
// Read receipt structure
|
||||||
@@ -231,6 +294,15 @@ struct ReadReceipt: Codable {
|
|||||||
self.timestamp = Date()
|
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? {
|
func encode() -> Data? {
|
||||||
try? JSONEncoder().encode(self)
|
try? JSONEncoder().encode(self)
|
||||||
}
|
}
|
||||||
@@ -238,6 +310,56 @@ struct ReadReceipt: Codable {
|
|||||||
static func decode(from data: Data) -> ReadReceipt? {
|
static func decode(from data: Data) -> ReadReceipt? {
|
||||||
try? JSONDecoder().decode(ReadReceipt.self, from: data)
|
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
|
// Channel key verification request
|
||||||
@@ -254,6 +376,14 @@ struct ChannelKeyVerifyRequest: Codable {
|
|||||||
self.timestamp = Date()
|
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? {
|
func encode() -> Data? {
|
||||||
return try? JSONEncoder().encode(self)
|
return try? JSONEncoder().encode(self)
|
||||||
}
|
}
|
||||||
@@ -261,6 +391,50 @@ struct ChannelKeyVerifyRequest: Codable {
|
|||||||
static func decode(from data: Data) -> ChannelKeyVerifyRequest? {
|
static func decode(from data: Data) -> ChannelKeyVerifyRequest? {
|
||||||
try? JSONDecoder().decode(ChannelKeyVerifyRequest.self, from: data)
|
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
|
// Channel key verification response
|
||||||
@@ -277,6 +451,14 @@ struct ChannelKeyVerifyResponse: Codable {
|
|||||||
self.timestamp = Date()
|
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? {
|
func encode() -> Data? {
|
||||||
return try? JSONEncoder().encode(self)
|
return try? JSONEncoder().encode(self)
|
||||||
}
|
}
|
||||||
@@ -284,6 +466,52 @@ struct ChannelKeyVerifyResponse: Codable {
|
|||||||
static func decode(from data: Data) -> ChannelKeyVerifyResponse? {
|
static func decode(from data: Data) -> ChannelKeyVerifyResponse? {
|
||||||
try? JSONDecoder().decode(ChannelKeyVerifyResponse.self, from: data)
|
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)
|
// Channel password update (sent by owner to members)
|
||||||
@@ -304,6 +532,16 @@ struct ChannelPasswordUpdate: Codable {
|
|||||||
self.timestamp = Date()
|
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? {
|
func encode() -> Data? {
|
||||||
return try? JSONEncoder().encode(self)
|
return try? JSONEncoder().encode(self)
|
||||||
}
|
}
|
||||||
@@ -311,6 +549,56 @@ struct ChannelPasswordUpdate: Codable {
|
|||||||
static func decode(from data: Data) -> ChannelPasswordUpdate? {
|
static func decode(from data: Data) -> ChannelPasswordUpdate? {
|
||||||
try? JSONDecoder().decode(ChannelPasswordUpdate.self, from: data)
|
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
|
// Channel metadata announcement
|
||||||
@@ -331,6 +619,16 @@ struct ChannelMetadata: Codable {
|
|||||||
self.keyCommitment = keyCommitment
|
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? {
|
func encode() -> Data? {
|
||||||
return try? JSONEncoder().encode(self)
|
return try? JSONEncoder().encode(self)
|
||||||
}
|
}
|
||||||
@@ -338,6 +636,75 @@ struct ChannelMetadata: Codable {
|
|||||||
static func decode(from data: Data) -> ChannelMetadata? {
|
static func decode(from data: Data) -> ChannelMetadata? {
|
||||||
try? JSONDecoder().decode(ChannelMetadata.self, from: data)
|
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
|
// MARK: - Peer Identity Rotation
|
||||||
@@ -369,6 +736,97 @@ struct NoiseIdentityAnnouncement: Codable {
|
|||||||
static func decode(from data: Data) -> NoiseIdentityAnnouncement? {
|
static func decode(from data: Data) -> NoiseIdentityAnnouncement? {
|
||||||
return try? JSONDecoder().decode(NoiseIdentityAnnouncement.self, from: data)
|
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
|
// Binding between ephemeral peer ID and cryptographic identity
|
||||||
@@ -447,6 +905,76 @@ struct VersionHello: Codable {
|
|||||||
static func decode(from data: Data) -> VersionHello? {
|
static func decode(from data: Data) -> VersionHello? {
|
||||||
try? JSONDecoder().decode(VersionHello.self, from: data)
|
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..<versionCount {
|
||||||
|
guard let version = dataCopy.readUInt8(at: &offset) else { return nil }
|
||||||
|
supportedVersions.append(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let preferredVersion = dataCopy.readUInt8(at: &offset),
|
||||||
|
let clientVersion = dataCopy.readString(at: &offset),
|
||||||
|
let platform = dataCopy.readString(at: &offset) else { return nil }
|
||||||
|
|
||||||
|
var capabilities: [String]? = nil
|
||||||
|
if hasCapabilities {
|
||||||
|
guard let capCount = dataCopy.readUInt8(at: &offset) else { return nil }
|
||||||
|
capabilities = []
|
||||||
|
for _ in 0..<capCount {
|
||||||
|
guard let capability = dataCopy.readString(at: &offset) else { return nil }
|
||||||
|
capabilities?.append(capability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return VersionHello(supportedVersions: supportedVersions,
|
||||||
|
preferredVersion: preferredVersion,
|
||||||
|
clientVersion: clientVersion,
|
||||||
|
platform: platform,
|
||||||
|
capabilities: capabilities)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version negotiation acknowledgment
|
// Version negotiation acknowledgment
|
||||||
@@ -479,6 +1007,79 @@ struct VersionAck: Codable {
|
|||||||
static func decode(from data: Data) -> VersionAck? {
|
static func decode(from data: Data) -> VersionAck? {
|
||||||
try? JSONDecoder().decode(VersionAck.self, from: data)
|
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..<capCount {
|
||||||
|
guard let capability = dataCopy.readString(at: &offset) else { return nil }
|
||||||
|
capabilities?.append(capability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reason: String? = nil
|
||||||
|
if hasReason {
|
||||||
|
reason = dataCopy.readString(at: &offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return VersionAck(agreedVersion: agreedVersion,
|
||||||
|
serverVersion: serverVersion,
|
||||||
|
platform: platform,
|
||||||
|
capabilities: capabilities,
|
||||||
|
rejected: rejected,
|
||||||
|
reason: reason)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delivery status for messages
|
// Delivery status for messages
|
||||||
|
|||||||
@@ -18,32 +18,7 @@ import IOKit.ps
|
|||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Extension for hex encoding/decoding
|
// Hex encoding/decoding is now in BinaryEncodingUtils.swift
|
||||||
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..<len {
|
|
||||||
let nextIndex = hexString.index(index, offsetBy: 2)
|
|
||||||
guard let byte = UInt8(String(hexString[index..<nextIndex]), radix: 16) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
data.append(byte)
|
|
||||||
index = nextIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
self = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extension for TimeInterval to Data conversion
|
// Extension for TimeInterval to Data conversion
|
||||||
extension TimeInterval {
|
extension TimeInterval {
|
||||||
@@ -928,9 +903,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
// Encode the ACK
|
// Encode the ACK
|
||||||
guard let ackData = ack.encode() else {
|
let ackData = ack.toBinaryData()
|
||||||
return
|
print("📤 Sending delivery ACK for message \(ack.originalMessageID) to \(recipientID) - binary size: \(ackData.count)")
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have a Noise session with this peer
|
// Check if we have a Noise session with this peer
|
||||||
// Use noiseService directly
|
// Use noiseService directly
|
||||||
@@ -995,9 +969,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
// Encode the receipt
|
// Encode the receipt
|
||||||
guard let receiptData = receipt.encode() else {
|
let receiptData = receipt.toBinaryData()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have a Noise session with this peer
|
// Check if we have a Noise session with this peer
|
||||||
// Use noiseService directly
|
// Use noiseService directly
|
||||||
@@ -1090,7 +1062,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
guard let metadataData = metadata.encode() else { return }
|
let metadataData = metadata.toBinaryData()
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.channelMetadata.rawValue,
|
type: MessageType.channelMetadata.rawValue,
|
||||||
@@ -1808,6 +1780,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
myNickname: myNickname,
|
myNickname: myNickname,
|
||||||
hopCount: UInt8(self.maxTTL - packet.ttl)
|
hopCount: UInt8(self.maxTTL - packet.ttl)
|
||||||
) {
|
) {
|
||||||
|
print("🔔 Generating delivery ACK for channel mention message \(messageWithPeerID.id)")
|
||||||
self.sendDeliveryAck(ack, to: senderID)
|
self.sendDeliveryAck(ack, to: senderID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1902,6 +1875,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
myNickname: myNickname,
|
myNickname: myNickname,
|
||||||
hopCount: UInt8(self.maxTTL - packet.ttl)
|
hopCount: UInt8(self.maxTTL - packet.ttl)
|
||||||
) {
|
) {
|
||||||
|
print("💬 Generating delivery ACK for private message \(messageWithPeerID.id) from \(senderID)")
|
||||||
self.sendDeliveryAck(ack, to: senderID)
|
self.sendDeliveryAck(ack, to: senderID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2222,11 +2196,19 @@ class BluetoothMeshService: NSObject {
|
|||||||
// This ACK is for us
|
// This ACK is for us
|
||||||
let senderID = packet.senderID.hexEncodedString()
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
// Check if payload is already decrypted (came through Noise)
|
// Check if payload is already decrypted (came through Noise)
|
||||||
if let ack = DeliveryAck.decode(from: packet.payload) {
|
if let ack = DeliveryAck.fromBinaryData(packet.payload) {
|
||||||
// Already decrypted - process directly
|
// Already decrypted - process directly
|
||||||
DeliveryTracker.shared.processDeliveryAck(ack)
|
DeliveryTracker.shared.processDeliveryAck(ack)
|
||||||
|
|
||||||
|
|
||||||
|
// Notify delegate
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.didReceiveDeliveryAck(ack)
|
||||||
|
}
|
||||||
|
} else if let ack = DeliveryAck.decode(from: packet.payload) {
|
||||||
|
// Fallback to JSON for backward compatibility
|
||||||
|
DeliveryTracker.shared.processDeliveryAck(ack)
|
||||||
|
|
||||||
// Notify delegate
|
// Notify delegate
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.didReceiveDeliveryAck(ack)
|
self.delegate?.didReceiveDeliveryAck(ack)
|
||||||
@@ -2235,11 +2217,19 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Try legacy decryption
|
// Try legacy decryption
|
||||||
do {
|
do {
|
||||||
let decryptedData = try noiseService.decrypt(packet.payload, from: senderID)
|
let decryptedData = try noiseService.decrypt(packet.payload, from: senderID)
|
||||||
if let ack = DeliveryAck.decode(from: decryptedData) {
|
if let ack = DeliveryAck.fromBinaryData(decryptedData) {
|
||||||
// Process the ACK
|
// Process the ACK
|
||||||
DeliveryTracker.shared.processDeliveryAck(ack)
|
DeliveryTracker.shared.processDeliveryAck(ack)
|
||||||
|
|
||||||
|
|
||||||
|
// Notify delegate
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.didReceiveDeliveryAck(ack)
|
||||||
|
}
|
||||||
|
} else if let ack = DeliveryAck.decode(from: decryptedData) {
|
||||||
|
// Fallback to JSON
|
||||||
|
DeliveryTracker.shared.processDeliveryAck(ack)
|
||||||
|
|
||||||
// Notify delegate
|
// Notify delegate
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.didReceiveDeliveryAck(ack)
|
self.delegate?.didReceiveDeliveryAck(ack)
|
||||||
@@ -2270,20 +2260,30 @@ class BluetoothMeshService: NSObject {
|
|||||||
// This read receipt is for us
|
// This read receipt is for us
|
||||||
let senderID = packet.senderID.hexEncodedString()
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
// Check if payload is already decrypted (came through Noise)
|
// Check if payload is already decrypted (came through Noise)
|
||||||
if let receipt = ReadReceipt.decode(from: packet.payload) {
|
if let receipt = ReadReceipt.fromBinaryData(packet.payload) {
|
||||||
// Already decrypted - process directly
|
// Already decrypted - process directly
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.didReceiveReadReceipt(receipt)
|
self.delegate?.didReceiveReadReceipt(receipt)
|
||||||
}
|
}
|
||||||
|
} else if let receipt = ReadReceipt.decode(from: packet.payload) {
|
||||||
|
// Fallback to JSON for backward compatibility
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.didReceiveReadReceipt(receipt)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Try legacy decryption
|
// Try legacy decryption
|
||||||
do {
|
do {
|
||||||
let decryptedData = try noiseService.decrypt(packet.payload, from: senderID)
|
let decryptedData = try noiseService.decrypt(packet.payload, from: senderID)
|
||||||
if let receipt = ReadReceipt.decode(from: decryptedData) {
|
if let receipt = ReadReceipt.fromBinaryData(decryptedData) {
|
||||||
// Process the read receipt
|
// Process the read receipt
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.didReceiveReadReceipt(receipt)
|
self.delegate?.didReceiveReadReceipt(receipt)
|
||||||
}
|
}
|
||||||
|
} else if let receipt = ReadReceipt.decode(from: decryptedData) {
|
||||||
|
// Fallback to JSON
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.didReceiveReadReceipt(receipt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Failed to decrypt read receipt - might be from unknown sender
|
// Failed to decrypt read receipt - might be from unknown sender
|
||||||
@@ -2300,8 +2300,24 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Handle Noise identity announcement
|
// Handle Noise identity announcement
|
||||||
let senderID = packet.senderID.hexEncodedString()
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
if senderID != myPeerID && !isPeerIDOurs(senderID) {
|
if senderID != myPeerID && !isPeerIDOurs(senderID) {
|
||||||
|
// Create defensive copy and validate
|
||||||
|
let payloadCopy = Data(packet.payload)
|
||||||
|
|
||||||
|
guard !payloadCopy.isEmpty else {
|
||||||
|
SecurityLogger.log("Received empty NoiseIdentityAnnouncement from \(senderID)", category: SecurityLogger.noise, level: .error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Decode the announcement
|
// Decode the announcement
|
||||||
guard let announcement = NoiseIdentityAnnouncement.decode(from: packet.payload) else {
|
let announcement: NoiseIdentityAnnouncement?
|
||||||
|
if let firstByte = payloadCopy.first, firstByte == 0x7B { // '{' character - JSON
|
||||||
|
announcement = NoiseIdentityAnnouncement.decode(from: payloadCopy) ?? NoiseIdentityAnnouncement.fromBinaryData(payloadCopy)
|
||||||
|
} else {
|
||||||
|
announcement = NoiseIdentityAnnouncement.fromBinaryData(payloadCopy) ?? NoiseIdentityAnnouncement.decode(from: payloadCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let announcement = announcement else {
|
||||||
|
SecurityLogger.log("Failed to decode NoiseIdentityAnnouncement from \(senderID), size: \(payloadCopy.count)", category: SecurityLogger.noise, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2363,13 +2379,27 @@ class BluetoothMeshService: NSObject {
|
|||||||
case .noiseHandshakeInit:
|
case .noiseHandshakeInit:
|
||||||
// Handle incoming Noise handshake initiation
|
// Handle incoming Noise handshake initiation
|
||||||
let senderID = packet.senderID.hexEncodedString()
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
|
print("🤝 Received Noise handshake init from \(senderID), payload size: \(packet.payload.count)")
|
||||||
|
|
||||||
// Check if this handshake is for us or broadcast
|
// Check if this handshake is for us or broadcast
|
||||||
if let recipientID = packet.recipientID,
|
if let recipientID = packet.recipientID,
|
||||||
!isPeerIDOurs(recipientID.hexEncodedString()) {
|
!isPeerIDOurs(recipientID.hexEncodedString()) {
|
||||||
// Not for us, ignore
|
// Not for us, relay if TTL > 0
|
||||||
|
if packet.ttl > 0 {
|
||||||
|
print("🔀 Relaying handshake init packet, TTL: \(packet.ttl)")
|
||||||
|
var relayPacket = packet
|
||||||
|
relayPacket.ttl -= 1
|
||||||
|
broadcastPacket(relayPacket)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !isPeerIDOurs(senderID) {
|
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
|
// Check if we've completed version negotiation with this peer
|
||||||
if negotiatedVersions[senderID] == nil {
|
if negotiatedVersions[senderID] == nil {
|
||||||
// Legacy peer - assume version 1 for backward compatibility
|
// Legacy peer - assume version 1 for backward compatibility
|
||||||
@@ -2384,13 +2414,26 @@ class BluetoothMeshService: NSObject {
|
|||||||
case .noiseHandshakeResp:
|
case .noiseHandshakeResp:
|
||||||
// Handle Noise handshake response
|
// Handle Noise handshake response
|
||||||
let senderID = packet.senderID.hexEncodedString()
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
|
print("🤝 Received Noise handshake response from \(senderID)")
|
||||||
|
|
||||||
// Check if this handshake response is for us
|
// Check if this handshake response is for us
|
||||||
if let recipientID = packet.recipientID,
|
if let recipientID = packet.recipientID {
|
||||||
!isPeerIDOurs(recipientID.hexEncodedString()) {
|
let recipientIDStr = recipientID.hexEncodedString()
|
||||||
// Not for us, ignore
|
print("🤝 Response targeted to: \(recipientIDStr), is us: \(isPeerIDOurs(recipientIDStr))")
|
||||||
return
|
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) {
|
if !isPeerIDOurs(senderID) {
|
||||||
|
print("🤝 Processing handshake response from \(senderID)")
|
||||||
handleNoiseHandshakeMessage(from: senderID, message: packet.payload, isInitiation: false)
|
handleNoiseHandshakeMessage(from: senderID, message: packet.payload, isInitiation: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3347,11 +3390,14 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
guard let self = self,
|
guard let self = self,
|
||||||
let pendingMessages = self.pendingPrivateMessages[peerID] else { return }
|
let pendingMessages = self.pendingPrivateMessages[peerID] else { return }
|
||||||
|
|
||||||
|
print("📬 Sending \(pendingMessages.count) pending private messages to \(peerID)")
|
||||||
|
|
||||||
// Clear pending messages for this peer
|
// Clear pending messages for this peer
|
||||||
self.pendingPrivateMessages.removeValue(forKey: peerID)
|
self.pendingPrivateMessages.removeValue(forKey: peerID)
|
||||||
|
|
||||||
// Send each pending message
|
// Send each pending message
|
||||||
for (content, recipientNickname, messageID) in pendingMessages {
|
for (content, recipientNickname, messageID) in pendingMessages {
|
||||||
|
print("📬 Sending pending message \(messageID) to \(peerID)")
|
||||||
// Use async to avoid blocking the queue
|
// Use async to avoid blocking the queue
|
||||||
DispatchQueue.global().async { [weak self] in
|
DispatchQueue.global().async { [weak self] in
|
||||||
self?.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
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) {
|
private func handleNoiseHandshakeMessage(from peerID: String, message: Data, isInitiation: Bool) {
|
||||||
// Use noiseService directly
|
// Use noiseService directly
|
||||||
|
print("🤝 handleNoiseHandshakeMessage from \(peerID), isInitiation: \(isInitiation), messageSize: \(message.count)")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Process handshake message
|
// Process handshake message
|
||||||
if let response = try noiseService.processHandshakeMessage(from: peerID, message: 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
|
// Always send responses as handshake response type
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseHandshakeResp.rawValue,
|
type: MessageType.noiseHandshakeResp.rawValue,
|
||||||
@@ -3429,10 +3477,13 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
// Use broadcastPacket instead of sendPacket to ensure it goes through the mesh
|
// Use broadcastPacket instead of sendPacket to ensure it goes through the mesh
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
|
} else {
|
||||||
|
print("🤝 No response needed from processHandshakeMessage")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if handshake is complete
|
// Check if handshake is complete
|
||||||
if noiseService.hasEstablishedSession(with: peerID) {
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
|
print("🎉 Handshake completed with peer: \(peerID)")
|
||||||
// Unlock rotation now that handshake is complete
|
// Unlock rotation now that handshake is complete
|
||||||
unlockRotation()
|
unlockRotation()
|
||||||
|
|
||||||
@@ -3462,8 +3513,10 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
} catch NoiseSessionError.alreadyEstablished {
|
} catch NoiseSessionError.alreadyEstablished {
|
||||||
// Session already established, ignore handshake
|
// Session already established, ignore handshake
|
||||||
|
print("🤝 Handshake already established with \(peerID)")
|
||||||
} catch {
|
} catch {
|
||||||
// Handshake failed
|
// Handshake failed
|
||||||
|
print("❌ Handshake failed with \(peerID): \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3492,7 +3545,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
// Decrypt the message
|
// Decrypt the message
|
||||||
|
print("🔓 Attempting to decrypt Noise message from \(peerID), encrypted size: \(encryptedData.count)")
|
||||||
let decryptedData = try noiseService.decrypt(encryptedData, from: peerID)
|
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)
|
// Check if this is a special format message (type marker + payload)
|
||||||
if decryptedData.count > 1 {
|
if decryptedData.count > 1 {
|
||||||
@@ -3503,8 +3558,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Extract the ACK JSON data (skip the type marker)
|
// Extract the ACK JSON data (skip the type marker)
|
||||||
let ackData = decryptedData.dropFirst()
|
let ackData = decryptedData.dropFirst()
|
||||||
|
|
||||||
// Decode the delivery ACK
|
// Decode the delivery ACK - try binary first, then JSON
|
||||||
if let ack = DeliveryAck.decode(from: ackData) {
|
if let ack = DeliveryAck.fromBinaryData(ackData) {
|
||||||
|
print("📨 Received binary delivery ACK via Noise: \(ack.originalMessageID) from \(ack.recipientNickname)")
|
||||||
|
|
||||||
// Process the ACK
|
// Process the ACK
|
||||||
DeliveryTracker.shared.processDeliveryAck(ack)
|
DeliveryTracker.shared.processDeliveryAck(ack)
|
||||||
@@ -3514,28 +3570,48 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
self.delegate?.didReceiveDeliveryAck(ack)
|
self.delegate?.didReceiveDeliveryAck(ack)
|
||||||
}
|
}
|
||||||
return
|
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)
|
// Try to parse as a full inner packet (for backward compatibility and other message types)
|
||||||
if let innerPacket = BitchatPacket.from(decryptedData) {
|
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
|
// Process the decrypted inner packet
|
||||||
// The packet will be handled according to its recipient ID
|
// The packet will be handled according to its recipient ID
|
||||||
// If it's for us, it won't be relayed
|
// If it's for us, it won't be relayed
|
||||||
handleReceivedPacket(innerPacket, from: peerID)
|
handleReceivedPacket(innerPacket, from: peerID)
|
||||||
|
} else {
|
||||||
|
print("⚠️ Failed to parse inner packet from decrypted data")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Failed to decrypt - might need to re-establish session
|
// Failed to decrypt - might need to re-establish session
|
||||||
|
print("❌ Failed to decrypt Noise message from \(peerID): \(error)")
|
||||||
if !noiseService.hasEstablishedSession(with: peerID) {
|
if !noiseService.hasEstablishedSession(with: peerID) {
|
||||||
|
print("🔄 No Noise session with \(peerID), initiating handshake")
|
||||||
initiateNoiseHandshake(with: peerID)
|
initiateNoiseHandshake(with: peerID)
|
||||||
|
} else {
|
||||||
|
print("⚠️ Have session with \(peerID) but decryption failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleChannelKeyVerifyRequest(from peerID: String, data: Data) {
|
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
|
// Forward to delegate (ChatViewModel) to handle
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@@ -3544,7 +3620,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleChannelKeyVerifyResponse(from peerID: String, data: Data) {
|
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
|
// Forward to delegate (ChatViewModel) to handle
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@@ -3561,7 +3637,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
let decryptedData = try noiseService.decrypt(data, from: peerID)
|
let decryptedData = try noiseService.decrypt(data, from: peerID)
|
||||||
|
|
||||||
// Parse the password update
|
// 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
|
// Forward to delegate (ChatViewModel) to handle
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@@ -3573,7 +3649,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
private func handleChannelMetadata(from peerID: String, data: Data) {
|
private func handleChannelMetadata(from peerID: String, data: Data) {
|
||||||
// Channel metadata is broadcast unencrypted (like channel announcements)
|
// 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
|
// Forward to delegate (ChatViewModel) to handle
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@@ -3584,7 +3660,26 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// MARK: - Protocol Version Negotiation
|
// MARK: - Protocol Version Negotiation
|
||||||
|
|
||||||
private func handleVersionHello(from peerID: String, data: Data, peripheral: CBPeripheral? = nil) {
|
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)
|
SecurityLogger.log("Failed to decode version hello from \(peerID)", category: SecurityLogger.session, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -3636,7 +3731,24 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleVersionAck(from peerID: String, data: Data) {
|
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)
|
SecurityLogger.log("Failed to decode version ack from \(peerID)", category: SecurityLogger.session, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -3678,7 +3790,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
platform: getPlatformString()
|
platform: getPlatformString()
|
||||||
)
|
)
|
||||||
|
|
||||||
guard let helloData = hello.encode() else { return }
|
let helloData = hello.toBinaryData()
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.versionHello.rawValue,
|
type: MessageType.versionHello.rawValue,
|
||||||
@@ -3704,7 +3816,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func sendVersionAck(_ ack: VersionAck, to peerID: String) {
|
private func sendVersionAck(_ ack: VersionAck, to peerID: String) {
|
||||||
guard let ackData = ack.encode() else { return }
|
let ackData = ack.toBinaryData()
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.versionAck.rawValue,
|
type: MessageType.versionAck.rawValue,
|
||||||
@@ -3730,7 +3842,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, to peers: [String]) {
|
func sendChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, to peers: [String]) {
|
||||||
guard let requestData = request.encode() else { return }
|
let requestData = request.toBinaryData()
|
||||||
|
|
||||||
// Send to each peer
|
// Send to each peer
|
||||||
for peerID in peers {
|
for peerID in peers {
|
||||||
@@ -3749,7 +3861,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, to peerID: String) {
|
func sendChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, to peerID: String) {
|
||||||
guard let responseData = response.encode() else { return }
|
let responseData = response.toBinaryData()
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.channelKeyVerifyResponse.rawValue,
|
type: MessageType.channelKeyVerifyResponse.rawValue,
|
||||||
@@ -3784,7 +3896,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
newKeyCommitment: newCommitment
|
newKeyCommitment: newCommitment
|
||||||
)
|
)
|
||||||
|
|
||||||
guard let updateData = update.encode() else { return }
|
let updateData = update.toBinaryData()
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Encrypt the entire update message
|
// Encrypt the entire update message
|
||||||
@@ -3852,9 +3964,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Encode the announcement
|
// Encode the announcement
|
||||||
guard let announcementData = announcement.encode() else {
|
let announcementData = announcement.toBinaryData()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseIdentityAnnounce.rawValue,
|
type: MessageType.noiseIdentityAnnounce.rawValue,
|
||||||
@@ -3960,7 +4070,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
// Encrypt with Noise
|
// Encrypt with Noise
|
||||||
|
print("🔐 Encrypting private message \(msgID) for \(recipientPeerID)")
|
||||||
let encryptedData = try noiseService.encrypt(innerData, for: recipientPeerID)
|
let encryptedData = try noiseService.encrypt(innerData, for: recipientPeerID)
|
||||||
|
print("🔐 Successfully encrypted message, size: \(encryptedData.count)")
|
||||||
|
|
||||||
// Send as Noise encrypted message
|
// Send as Noise encrypted message
|
||||||
let outerPacket = BitchatPacket(
|
let outerPacket = BitchatPacket(
|
||||||
@@ -3973,9 +4085,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
ttl: adaptiveTTL
|
ttl: adaptiveTTL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print("📤 Broadcasting encrypted private message \(msgID) to \(recipientPeerID)")
|
||||||
broadcastPacket(outerPacket)
|
broadcastPacket(outerPacket)
|
||||||
} catch {
|
} catch {
|
||||||
// Failed to encrypt message
|
// Failed to encrypt message
|
||||||
|
print("❌ Failed to encrypt private message \(msgID) for \(recipientPeerID): \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ class DeliveryTracker {
|
|||||||
// Don't track broadcasts or certain message types
|
// Don't track broadcasts or certain message types
|
||||||
guard message.isPrivate || message.channel != nil else { return }
|
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(
|
let delivery = PendingDelivery(
|
||||||
messageID: message.id,
|
messageID: message.id,
|
||||||
@@ -89,9 +91,21 @@ class DeliveryTracker {
|
|||||||
pendingDeliveries[message.id] = delivery
|
pendingDeliveries[message.id] = delivery
|
||||||
pendingLock.unlock()
|
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
|
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)
|
// Schedule timeout (outside of lock)
|
||||||
@@ -102,9 +116,11 @@ class DeliveryTracker {
|
|||||||
pendingLock.lock()
|
pendingLock.lock()
|
||||||
defer { pendingLock.unlock() }
|
defer { pendingLock.unlock() }
|
||||||
|
|
||||||
|
print("✅ Processing delivery ACK for message \(ack.originalMessageID) from \(ack.recipientNickname)")
|
||||||
|
|
||||||
// Prevent duplicate ACK processing
|
// Prevent duplicate ACK processing
|
||||||
guard !receivedAckIDs.contains(ack.ackID) else {
|
guard !receivedAckIDs.contains(ack.ackID) else {
|
||||||
|
print("⚠️ Duplicate ACK \(ack.ackID) - ignoring")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
receivedAckIDs.insert(ack.ackID)
|
receivedAckIDs.insert(ack.ackID)
|
||||||
@@ -112,6 +128,7 @@ class DeliveryTracker {
|
|||||||
// Find the pending delivery
|
// Find the pending delivery
|
||||||
guard var delivery = pendingDeliveries[ack.originalMessageID] else {
|
guard var delivery = pendingDeliveries[ack.originalMessageID] else {
|
||||||
// Message might have already been delivered or timed out
|
// Message might have already been delivered or timed out
|
||||||
|
print("⚠️ No pending delivery found for message \(ack.originalMessageID)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +153,7 @@ class DeliveryTracker {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Direct message - mark as delivered
|
// 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()))
|
updateDeliveryStatus(ack.originalMessageID, status: .delivered(to: ack.recipientNickname, at: Date()))
|
||||||
pendingDeliveries.removeValue(forKey: ack.originalMessageID)
|
pendingDeliveries.removeValue(forKey: ack.originalMessageID)
|
||||||
}
|
}
|
||||||
@@ -180,6 +198,7 @@ class DeliveryTracker {
|
|||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
|
|
||||||
private func updateDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
private func updateDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
||||||
|
print("📊 Updating delivery status for message \(messageID): \(status)")
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.deliveryStatusUpdated.send((messageID: messageID, status: status))
|
self?.deliveryStatusUpdated.send((messageID: messageID, status: status))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,11 @@ class NoiseEncryptionService {
|
|||||||
return sessionManager.getSession(for: peerID)?.isEstablished() ?? false
|
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
|
// MARK: - Encryption/Decryption
|
||||||
|
|
||||||
/// Encrypt data for a specific peer
|
/// Encrypt data for a specific peer
|
||||||
@@ -459,6 +464,31 @@ struct NoiseMessage: Codable {
|
|||||||
return nil
|
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
|
// MARK: - Errors
|
||||||
|
|||||||
@@ -3654,6 +3654,7 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
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
|
// Helper function to check if we should skip this update
|
||||||
func shouldSkipUpdate(currentStatus: DeliveryStatus?, newStatus: DeliveryStatus) -> Bool {
|
func shouldSkipUpdate(currentStatus: DeliveryStatus?, newStatus: DeliveryStatus) -> Bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user