mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 15:45:20 +00:00
Refactor/robustness (#446)
* Refactor BitChat for improved robustness and performance Major refactoring to simplify architecture and fix critical issues: Architecture Improvements: - Replace complex BluetoothMeshService with simplified SimplifiedBluetoothService - Consolidate message routing and peer management into unified services - Remove redundant caching layers and optimize performance Bug Fixes: - Fix critical BLE peer mapping corruption in mesh networks - Fix encrypted message routing failures in multi-peer scenarios - Fix app freezes and Main Thread Checker warnings - Fix BLE message delivery in dual-role connections - Fix favorite toggle UI not updating instantly - Fix Nostr offline messaging with 24-hour message filtering Features: - Add command processor for chat commands - Add autocomplete service for mentions and commands - Improve private chat management with dedicated service - Add unified peer service for consistent state management Performance: - Optimize BLE reconnection speed - Reduce excessive logging throughout codebase - Improve message deduplication efficiency - Optimize UI updates and state management * Improvements refactor robust (#441) * remove unused code * remove more * TLV for announcement * restore * restore * restore? * messages tlv too (#442) * Fix Nostr notification and read receipt issues, add TLV encoding, cleanup unused code ## Notification & Read Receipt Fixes - Fixed toolbar notification icon appearing incorrectly on app restart for already-read messages - Fixed read receipts being incorrectly deleted on startup when privateChats was empty - Fixed messages not being marked as read when opening chat for first time - Fixed senderPeerID not being updated during message consolidation - Added startup phase logic to block old messages (>30s) while allowing recent ones - Fixed unread status checking across all storage locations (ephemeral, stable Noise keys, temporary Nostr IDs) ## TLV Encoding Implementation - Implemented Type-Length-Value encoding for private message payloads - Added PrivateMessagePacket struct with TLV encode/decode methods - Enhanced message structure for better extensibility and robustness ## Code Cleanup - Removed unused PeerStateManager class and related dependencies - Removed dead protocol types (DeliveryAck, ProtocolAck/Nack, NoiseIdentityAnnouncement) - Cleaned up BitchatDelegate by removing unused methods - Removed excessive debug logging throughout ChatViewModel - Added test-only ProtocolNack helper for integration tests ## Technical Details - Messages stored under three ID types: ephemeral peer IDs, stable Noise key hexes, temporary Nostr IDs - Fixed cleanupOldReadReceipts() to skip when privateChats is empty or during startup - Updated message consolidation to properly update senderPeerID - Restored NIP-17 timestamp randomization (±15 minutes) for privacy --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
@@ -209,7 +209,7 @@ struct BinaryProtocol {
|
||||
|
||||
// Decode binary data to BitchatPacket
|
||||
static func decode(_ data: Data) -> BitchatPacket? {
|
||||
// Remove padding first
|
||||
// Remove padding first and create a defensive copy
|
||||
let unpaddedData = MessagePadding.unpad(data)
|
||||
|
||||
// Minimum size check: header + senderID
|
||||
@@ -217,41 +217,43 @@ struct BinaryProtocol {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert to array for safer indexed access
|
||||
let dataArray = Array(unpaddedData)
|
||||
var offset = 0
|
||||
|
||||
// Header parsing with bounds checks
|
||||
guard offset + 1 <= unpaddedData.count else { return nil }
|
||||
let version = unpaddedData[offset]; offset += 1
|
||||
guard offset < dataArray.count else { return nil }
|
||||
let version = dataArray[offset]; offset += 1
|
||||
|
||||
// Check if version is 1 (only supported version)
|
||||
guard version == 1 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard offset + 1 <= unpaddedData.count else { return nil }
|
||||
let type = unpaddedData[offset]; offset += 1
|
||||
guard offset < dataArray.count else { return nil }
|
||||
let type = dataArray[offset]; offset += 1
|
||||
|
||||
guard offset + 1 <= unpaddedData.count else { return nil }
|
||||
let ttl = unpaddedData[offset]; offset += 1
|
||||
guard offset < dataArray.count else { return nil }
|
||||
let ttl = dataArray[offset]; offset += 1
|
||||
|
||||
// Timestamp - need 8 bytes
|
||||
guard offset + 8 <= unpaddedData.count else { return nil }
|
||||
let timestampData = unpaddedData[offset..<offset+8]
|
||||
guard offset + 8 <= dataArray.count else { return nil }
|
||||
let timestampData = Data(dataArray[offset..<offset+8])
|
||||
let timestamp = timestampData.reduce(0) { result, byte in
|
||||
(result << 8) | UInt64(byte)
|
||||
}
|
||||
offset += 8
|
||||
|
||||
// Flags
|
||||
guard offset + 1 <= unpaddedData.count else { return nil }
|
||||
let flags = unpaddedData[offset]; offset += 1
|
||||
guard offset < dataArray.count else { return nil }
|
||||
let flags = dataArray[offset]; offset += 1
|
||||
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
||||
let hasSignature = (flags & Flags.hasSignature) != 0
|
||||
let isCompressed = (flags & Flags.isCompressed) != 0
|
||||
|
||||
// Payload length - need 2 bytes
|
||||
guard offset + 2 <= unpaddedData.count else { return nil }
|
||||
let payloadLengthData = unpaddedData[offset..<offset+2]
|
||||
guard offset + 2 <= dataArray.count else { return nil }
|
||||
let payloadLengthData = Data(dataArray[offset..<offset+2])
|
||||
let payloadLength = payloadLengthData.reduce(0) { result, byte in
|
||||
(result << 8) | UInt16(byte)
|
||||
}
|
||||
@@ -261,15 +263,15 @@ struct BinaryProtocol {
|
||||
guard payloadLength <= 65535 else { return nil }
|
||||
|
||||
// SenderID - need 8 bytes
|
||||
guard offset + senderIDSize <= unpaddedData.count else { return nil }
|
||||
let senderID = unpaddedData[offset..<offset+senderIDSize]
|
||||
guard offset + senderIDSize <= dataArray.count else { return nil }
|
||||
let senderID = Data(dataArray[offset..<offset+senderIDSize])
|
||||
offset += senderIDSize
|
||||
|
||||
// RecipientID if present
|
||||
var recipientID: Data?
|
||||
if hasRecipient {
|
||||
guard offset + recipientIDSize <= unpaddedData.count else { return nil }
|
||||
recipientID = unpaddedData[offset..<offset+recipientIDSize]
|
||||
guard offset + recipientIDSize <= dataArray.count else { return nil }
|
||||
recipientID = Data(dataArray[offset..<offset+recipientIDSize])
|
||||
offset += recipientIDSize
|
||||
}
|
||||
|
||||
@@ -280,8 +282,8 @@ struct BinaryProtocol {
|
||||
guard Int(payloadLength) >= 2 else { return nil }
|
||||
|
||||
// Check we have enough data for the original size prefix
|
||||
guard offset + 2 <= unpaddedData.count else { return nil }
|
||||
let originalSizeData = unpaddedData[offset..<offset+2]
|
||||
guard offset + 2 <= dataArray.count else { return nil }
|
||||
let originalSizeData = Data(dataArray[offset..<offset+2])
|
||||
let originalSize = Int(originalSizeData.reduce(0) { result, byte in
|
||||
(result << 8) | UInt16(byte)
|
||||
})
|
||||
@@ -292,11 +294,11 @@ struct BinaryProtocol {
|
||||
|
||||
// Check we have enough data for the compressed payload
|
||||
let compressedPayloadSize = Int(payloadLength) - 2
|
||||
guard compressedPayloadSize >= 0 && offset + compressedPayloadSize <= unpaddedData.count else {
|
||||
guard compressedPayloadSize >= 0 && offset + compressedPayloadSize <= dataArray.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let compressedPayload = unpaddedData[offset..<offset+compressedPayloadSize]
|
||||
let compressedPayload = Data(dataArray[offset..<offset+compressedPayloadSize])
|
||||
offset += compressedPayloadSize
|
||||
|
||||
// Decompress with error handling
|
||||
@@ -312,23 +314,23 @@ struct BinaryProtocol {
|
||||
payload = decompressedPayload
|
||||
} else {
|
||||
// Uncompressed payload
|
||||
guard Int(payloadLength) >= 0 && offset + Int(payloadLength) <= unpaddedData.count else {
|
||||
guard Int(payloadLength) >= 0 && offset + Int(payloadLength) <= dataArray.count else {
|
||||
return nil
|
||||
}
|
||||
payload = unpaddedData[offset..<offset+Int(payloadLength)]
|
||||
payload = Data(dataArray[offset..<offset+Int(payloadLength)])
|
||||
offset += Int(payloadLength)
|
||||
}
|
||||
|
||||
// Signature if present
|
||||
var signature: Data?
|
||||
if hasSignature {
|
||||
guard offset + signatureSize <= unpaddedData.count else { return nil }
|
||||
signature = unpaddedData[offset..<offset+signatureSize]
|
||||
guard offset + signatureSize <= dataArray.count else { return nil }
|
||||
signature = Data(dataArray[offset..<offset+signatureSize])
|
||||
offset += signatureSize
|
||||
}
|
||||
|
||||
// Final validation: ensure we haven't gone past the end
|
||||
guard offset <= unpaddedData.count else { return nil }
|
||||
guard offset <= dataArray.count else { return nil }
|
||||
|
||||
return BitchatPacket(
|
||||
type: type,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
/// 1. **Creation**: Messages are created with type, content, and metadata
|
||||
/// 2. **Encoding**: Converted to binary format with proper field ordering
|
||||
/// 3. **Fragmentation**: Split if larger than BLE MTU (512 bytes)
|
||||
/// 4. **Transmission**: Sent via BluetoothMeshService
|
||||
/// 4. **Transmission**: Sent via SimplifiedBluetoothService
|
||||
/// 5. **Routing**: Relayed by intermediate nodes (TTL decrements)
|
||||
/// 6. **Reassembly**: Fragments collected and reassembled
|
||||
/// 7. **Decoding**: Binary data parsed back to message objects
|
||||
@@ -49,12 +49,12 @@
|
||||
/// - **Fragment**: Multi-part message handling
|
||||
/// - **Delivery/Read**: Message acknowledgments
|
||||
/// - **Noise**: Encrypted channel establishment
|
||||
/// - **Version**: Protocol compatibility negotiation
|
||||
/// - **Version**: Protocol version negotiation
|
||||
///
|
||||
/// ## Future Extensions
|
||||
/// The protocol is designed to be extensible:
|
||||
/// - Reserved message type ranges for future use
|
||||
/// - Version negotiation for backward compatibility
|
||||
/// - Version field for protocol evolution
|
||||
/// - Optional fields for new features
|
||||
///
|
||||
|
||||
@@ -95,21 +95,21 @@ struct MessagePadding {
|
||||
guard !data.isEmpty else { return data }
|
||||
|
||||
// Last byte tells us how much padding to remove
|
||||
let paddingLength = Int(data[data.count - 1])
|
||||
let lastIndex = data.count - 1
|
||||
guard lastIndex >= 0 else { return data }
|
||||
|
||||
let paddingLength = Int(data[lastIndex])
|
||||
guard paddingLength > 0 && paddingLength <= data.count else {
|
||||
// Debug logging for 243-byte packets
|
||||
if data.count == 243 {
|
||||
}
|
||||
// No valid padding, return original data
|
||||
return data
|
||||
}
|
||||
|
||||
let result = data.prefix(data.count - paddingLength)
|
||||
// Create a new Data object (not a subsequence) for thread safety
|
||||
let unpaddedLength = data.count - paddingLength
|
||||
guard unpaddedLength >= 0 else { return Data() }
|
||||
|
||||
// Debug logging for 243-byte packets
|
||||
if data.count == 243 {
|
||||
}
|
||||
|
||||
return result
|
||||
// Return a proper copy, not a subsequence
|
||||
return Data(data.prefix(unpaddedLength))
|
||||
}
|
||||
|
||||
// Find optimal block size for data
|
||||
@@ -132,57 +132,50 @@ struct MessagePadding {
|
||||
|
||||
// MARK: - Message Types
|
||||
|
||||
/// Defines all message types in the BitChat protocol.
|
||||
/// Each type has a unique identifier for efficient binary encoding.
|
||||
/// Types are grouped by function: user messages, protocol control, encryption, etc.
|
||||
/// Simplified BitChat protocol message types.
|
||||
/// Reduced from 24 types to just 6 essential ones.
|
||||
/// All private communication metadata (receipts, status) is embedded in noiseEncrypted payloads.
|
||||
enum MessageType: UInt8 {
|
||||
case announce = 0x01
|
||||
case leave = 0x03
|
||||
case message = 0x04 // All user messages (private and broadcast)
|
||||
case fragmentStart = 0x05
|
||||
case fragmentContinue = 0x06
|
||||
case fragmentEnd = 0x07
|
||||
case deliveryAck = 0x0A // Acknowledge message received
|
||||
case deliveryStatusRequest = 0x0B // Request delivery status update
|
||||
case readReceipt = 0x0C // Message has been read/viewed
|
||||
// Public messages (unencrypted)
|
||||
case announce = 0x01 // "I'm here" with nickname
|
||||
case message = 0x02 // Public chat message
|
||||
case leave = 0x03 // "I'm leaving"
|
||||
|
||||
// Noise Protocol messages
|
||||
case noiseHandshakeInit = 0x10 // Noise handshake initiation
|
||||
case noiseHandshakeResp = 0x11 // Noise handshake response
|
||||
case noiseEncrypted = 0x12 // Noise encrypted transport message
|
||||
case noiseIdentityAnnounce = 0x13 // Announce static public key for discovery
|
||||
// Noise encryption
|
||||
case noiseHandshake = 0x10 // Handshake (init or response determined by payload)
|
||||
case noiseEncrypted = 0x11 // All encrypted payloads (messages, receipts, etc.)
|
||||
|
||||
// Protocol-level acknowledgments
|
||||
case protocolAck = 0x22 // Generic protocol acknowledgment
|
||||
case protocolNack = 0x23 // Negative acknowledgment (failure)
|
||||
case systemValidation = 0x24 // Session validation ping
|
||||
case handshakeRequest = 0x25 // Request handshake for pending messages
|
||||
|
||||
// Favorite system messages
|
||||
case favorited = 0x30 // Peer favorited us
|
||||
case unfavorited = 0x31 // Peer unfavorited us
|
||||
// Fragmentation (simplified)
|
||||
case fragment = 0x20 // Single fragment type for large messages
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .announce: return "announce"
|
||||
case .leave: return "leave"
|
||||
case .message: return "message"
|
||||
case .fragmentStart: return "fragmentStart"
|
||||
case .fragmentContinue: return "fragmentContinue"
|
||||
case .fragmentEnd: return "fragmentEnd"
|
||||
case .deliveryAck: return "deliveryAck"
|
||||
case .deliveryStatusRequest: return "deliveryStatusRequest"
|
||||
case .readReceipt: return "readReceipt"
|
||||
case .noiseHandshakeInit: return "noiseHandshakeInit"
|
||||
case .noiseHandshakeResp: return "noiseHandshakeResp"
|
||||
case .leave: return "leave"
|
||||
case .noiseHandshake: return "noiseHandshake"
|
||||
case .noiseEncrypted: return "noiseEncrypted"
|
||||
case .noiseIdentityAnnounce: return "noiseIdentityAnnounce"
|
||||
case .protocolAck: return "protocolAck"
|
||||
case .protocolNack: return "protocolNack"
|
||||
case .systemValidation: return "systemValidation"
|
||||
case .handshakeRequest: return "handshakeRequest"
|
||||
case .favorited: return "favorited"
|
||||
case .unfavorited: return "unfavorited"
|
||||
case .fragment: return "fragment"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Noise Payload Types
|
||||
|
||||
/// Types of payloads embedded within noiseEncrypted messages.
|
||||
/// The first byte of decrypted Noise payload indicates the type.
|
||||
/// This provides privacy - observers can't distinguish message types.
|
||||
enum NoisePayloadType: UInt8 {
|
||||
// Messages and status
|
||||
case privateMessage = 0x01 // Private chat message
|
||||
case readReceipt = 0x02 // Message was read
|
||||
case delivered = 0x03 // Message was delivered
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .privateMessage: return "privateMessage"
|
||||
case .readReceipt: return "readReceipt"
|
||||
case .delivered: return "delivered"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,14 +191,8 @@ enum LazyHandshakeState {
|
||||
case failed(Error) // Handshake failed
|
||||
}
|
||||
|
||||
// MARK: - Special Recipients
|
||||
|
||||
/// Defines special recipient identifiers used in the protocol.
|
||||
/// These magic values indicate broadcast or system-level recipients
|
||||
/// rather than specific peer IDs.
|
||||
struct SpecialRecipients {
|
||||
static let broadcast = Data(repeating: 0xFF, count: 8) // All 0xFF = broadcast
|
||||
}
|
||||
// MARK: - Special Recipients (removed)
|
||||
// Previously defined broadcast identifiers were unused; removed for simplicity.
|
||||
|
||||
// MARK: - Core Protocol Structures
|
||||
|
||||
@@ -269,103 +256,8 @@ struct BitchatPacket: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delivery Acknowledgments
|
||||
|
||||
/// Acknowledgment sent when a message is successfully delivered to a recipient.
|
||||
/// Provides delivery confirmation for reliable messaging and UI feedback.
|
||||
/// - Note: Only sent for direct messages, not broadcasts
|
||||
struct DeliveryAck: Codable {
|
||||
let originalMessageID: String
|
||||
let ackID: String
|
||||
let recipientID: String // Who received it
|
||||
let recipientNickname: String
|
||||
let timestamp: Date
|
||||
let hopCount: UInt8 // How many hops to reach recipient
|
||||
|
||||
init(originalMessageID: String, recipientID: String, recipientNickname: String, hopCount: UInt8) {
|
||||
self.originalMessageID = originalMessageID
|
||||
self.ackID = UUID().uuidString
|
||||
self.recipientID = recipientID
|
||||
self.recipientNickname = recipientNickname
|
||||
self.timestamp = Date()
|
||||
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)
|
||||
}
|
||||
|
||||
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 InputValidator.validatePeerID(recipientID) else { return nil }
|
||||
|
||||
guard let hopCount = dataCopy.readUInt8(at: &offset),
|
||||
InputValidator.validateHopCount(hopCount),
|
||||
let timestamp = dataCopy.readDate(at: &offset),
|
||||
InputValidator.validateTimestamp(timestamp),
|
||||
let recipientNicknameRaw = dataCopy.readString(at: &offset),
|
||||
let recipientNickname = InputValidator.validateNickname(recipientNicknameRaw) else { return nil }
|
||||
|
||||
return DeliveryAck(originalMessageID: originalMessageID,
|
||||
ackID: ackID,
|
||||
recipientID: recipientID,
|
||||
recipientNickname: recipientNickname,
|
||||
timestamp: timestamp,
|
||||
hopCount: hopCount)
|
||||
}
|
||||
}
|
||||
// MARK: - Delivery Acknowledgments (removed)
|
||||
// Legacy DeliveryAck structures are no longer used; delivery status flows via Noise payloads.
|
||||
|
||||
// MARK: - Read Receipts
|
||||
|
||||
@@ -456,460 +348,8 @@ struct ReadReceipt: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handshake Requests
|
||||
|
||||
// Handshake request for pending messages
|
||||
struct HandshakeRequest: Codable {
|
||||
let requestID: String
|
||||
let requesterID: String // Who needs the handshake
|
||||
let requesterNickname: String // Nickname of requester
|
||||
let targetID: String // Who should initiate handshake
|
||||
let pendingMessageCount: UInt8 // Number of messages queued
|
||||
let timestamp: Date
|
||||
|
||||
init(requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UInt8) {
|
||||
self.requestID = UUID().uuidString
|
||||
self.requesterID = requesterID
|
||||
self.requesterNickname = requesterNickname
|
||||
self.targetID = targetID
|
||||
self.pendingMessageCount = pendingMessageCount
|
||||
self.timestamp = Date()
|
||||
}
|
||||
|
||||
// For binary decoding
|
||||
private init(requestID: String, requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UInt8, timestamp: Date) {
|
||||
self.requestID = requestID
|
||||
self.requesterID = requesterID
|
||||
self.requesterNickname = requesterNickname
|
||||
self.targetID = targetID
|
||||
self.pendingMessageCount = pendingMessageCount
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
// MARK: - Binary Encoding
|
||||
|
||||
func toBinaryData() -> Data {
|
||||
var data = Data()
|
||||
data.appendUUID(requestID)
|
||||
|
||||
// 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)
|
||||
|
||||
// TargetID as 8-byte hex string
|
||||
var targetData = Data()
|
||||
tempID = targetID
|
||||
while tempID.count >= 2 && targetData.count < 8 {
|
||||
let hexByte = String(tempID.prefix(2))
|
||||
if let byte = UInt8(hexByte, radix: 16) {
|
||||
targetData.append(byte)
|
||||
}
|
||||
tempID = String(tempID.dropFirst(2))
|
||||
}
|
||||
while targetData.count < 8 {
|
||||
targetData.append(0)
|
||||
}
|
||||
data.append(targetData)
|
||||
|
||||
data.appendUInt8(pendingMessageCount)
|
||||
data.appendDate(timestamp)
|
||||
data.appendString(requesterNickname)
|
||||
return data
|
||||
}
|
||||
|
||||
static func fromBinaryData(_ data: Data) -> HandshakeRequest? {
|
||||
// Create defensive copy
|
||||
let dataCopy = Data(data)
|
||||
|
||||
// Minimum size: UUID (16) + requesterID (8) + targetID (8) + count (1) + timestamp (8) + min nickname
|
||||
guard dataCopy.count >= 42 else { return nil }
|
||||
|
||||
var offset = 0
|
||||
|
||||
guard let requestID = dataCopy.readUUID(at: &offset) else { return nil }
|
||||
|
||||
guard let requesterIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||
let requesterID = requesterIDData.hexEncodedString()
|
||||
guard InputValidator.validatePeerID(requesterID) else { return nil }
|
||||
|
||||
guard let targetIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||
let targetID = targetIDData.hexEncodedString()
|
||||
guard InputValidator.validatePeerID(targetID) else { return nil }
|
||||
|
||||
guard let pendingMessageCount = dataCopy.readUInt8(at: &offset),
|
||||
let timestamp = dataCopy.readDate(at: &offset),
|
||||
InputValidator.validateTimestamp(timestamp),
|
||||
let requesterNicknameRaw = dataCopy.readString(at: &offset),
|
||||
let requesterNickname = InputValidator.validateNickname(requesterNicknameRaw) else { return nil }
|
||||
|
||||
return HandshakeRequest(requestID: requestID,
|
||||
requesterID: requesterID,
|
||||
requesterNickname: requesterNickname,
|
||||
targetID: targetID,
|
||||
pendingMessageCount: pendingMessageCount,
|
||||
timestamp: timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Acknowledgments
|
||||
|
||||
// Protocol-level acknowledgment for reliable delivery
|
||||
struct ProtocolAck: Codable {
|
||||
let originalPacketID: String // ID of the packet being acknowledged
|
||||
let ackID: String // Unique ID for this ACK
|
||||
let senderID: String // Who sent the original packet
|
||||
let receiverID: String // Who received and is acknowledging
|
||||
let packetType: UInt8 // Type of packet being acknowledged
|
||||
let timestamp: Date // When ACK was generated
|
||||
let hopCount: UInt8 // Hops taken to reach receiver
|
||||
|
||||
init(originalPacketID: String, senderID: String, receiverID: String, packetType: UInt8, hopCount: UInt8) {
|
||||
self.originalPacketID = originalPacketID
|
||||
self.ackID = UUID().uuidString
|
||||
self.senderID = senderID
|
||||
self.receiverID = receiverID
|
||||
self.packetType = packetType
|
||||
self.timestamp = Date()
|
||||
self.hopCount = hopCount
|
||||
}
|
||||
|
||||
// Private init for binary decoding
|
||||
private init(originalPacketID: String, ackID: String, senderID: String, receiverID: String,
|
||||
packetType: UInt8, timestamp: Date, hopCount: UInt8) {
|
||||
self.originalPacketID = originalPacketID
|
||||
self.ackID = ackID
|
||||
self.senderID = senderID
|
||||
self.receiverID = receiverID
|
||||
self.packetType = packetType
|
||||
self.timestamp = timestamp
|
||||
self.hopCount = hopCount
|
||||
}
|
||||
|
||||
func toBinaryData() -> Data {
|
||||
var data = Data()
|
||||
data.appendUUID(originalPacketID)
|
||||
data.appendUUID(ackID)
|
||||
|
||||
// Sender and receiver IDs as 8-byte hex strings
|
||||
data.append(Data(hexString: senderID) ?? Data(repeating: 0, count: 8))
|
||||
data.append(Data(hexString: receiverID) ?? Data(repeating: 0, count: 8))
|
||||
|
||||
data.appendUInt8(packetType)
|
||||
data.appendUInt8(hopCount)
|
||||
data.appendDate(timestamp)
|
||||
return data
|
||||
}
|
||||
|
||||
static func fromBinaryData(_ data: Data) -> ProtocolAck? {
|
||||
let dataCopy = Data(data)
|
||||
guard dataCopy.count >= 50 else { return nil } // 2 UUIDs + 2 IDs + type + hop + timestamp
|
||||
|
||||
var offset = 0
|
||||
guard let originalPacketID = dataCopy.readUUID(at: &offset),
|
||||
let ackID = dataCopy.readUUID(at: &offset),
|
||||
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
||||
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
||||
let packetType = dataCopy.readUInt8(at: &offset),
|
||||
InputValidator.validateMessageType(packetType),
|
||||
let hopCount = dataCopy.readUInt8(at: &offset),
|
||||
InputValidator.validateHopCount(hopCount),
|
||||
let timestamp = dataCopy.readDate(at: &offset),
|
||||
InputValidator.validateTimestamp(timestamp) else { return nil }
|
||||
|
||||
let senderID = senderIDData.hexEncodedString()
|
||||
let receiverID = receiverIDData.hexEncodedString()
|
||||
guard InputValidator.validatePeerID(senderID),
|
||||
InputValidator.validatePeerID(receiverID) else { return nil }
|
||||
|
||||
return ProtocolAck(originalPacketID: originalPacketID,
|
||||
ackID: ackID,
|
||||
senderID: senderID,
|
||||
receiverID: receiverID,
|
||||
packetType: packetType,
|
||||
timestamp: timestamp,
|
||||
hopCount: hopCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Protocol-level negative acknowledgment
|
||||
struct ProtocolNack: Codable {
|
||||
let originalPacketID: String // ID of the packet that failed
|
||||
let nackID: String // Unique ID for this NACK
|
||||
let senderID: String // Who sent the original packet
|
||||
let receiverID: String // Who is reporting the failure
|
||||
let packetType: UInt8 // Type of packet that failed
|
||||
let timestamp: Date // When NACK was generated
|
||||
let reason: String // Reason for failure
|
||||
let errorCode: UInt8 // Numeric error code
|
||||
|
||||
// Error codes
|
||||
enum ErrorCode: UInt8 {
|
||||
case unknown = 0
|
||||
case checksumFailed = 1
|
||||
case decryptionFailed = 2
|
||||
case malformedPacket = 3
|
||||
case unsupportedVersion = 4
|
||||
case resourceExhausted = 5
|
||||
case routingFailed = 6
|
||||
case sessionExpired = 7
|
||||
}
|
||||
|
||||
init(originalPacketID: String, senderID: String, receiverID: String,
|
||||
packetType: UInt8, reason: String, errorCode: ErrorCode = .unknown) {
|
||||
self.originalPacketID = originalPacketID
|
||||
self.nackID = UUID().uuidString
|
||||
self.senderID = senderID
|
||||
self.receiverID = receiverID
|
||||
self.packetType = packetType
|
||||
self.timestamp = Date()
|
||||
self.reason = reason
|
||||
self.errorCode = errorCode.rawValue
|
||||
}
|
||||
|
||||
// Private init for binary decoding
|
||||
private init(originalPacketID: String, nackID: String, senderID: String, receiverID: String,
|
||||
packetType: UInt8, timestamp: Date, reason: String, errorCode: UInt8) {
|
||||
self.originalPacketID = originalPacketID
|
||||
self.nackID = nackID
|
||||
self.senderID = senderID
|
||||
self.receiverID = receiverID
|
||||
self.packetType = packetType
|
||||
self.timestamp = timestamp
|
||||
self.reason = reason
|
||||
self.errorCode = errorCode
|
||||
}
|
||||
|
||||
func toBinaryData() -> Data {
|
||||
var data = Data()
|
||||
data.appendUUID(originalPacketID)
|
||||
data.appendUUID(nackID)
|
||||
|
||||
// Sender and receiver IDs as 8-byte hex strings
|
||||
data.append(Data(hexString: senderID) ?? Data(repeating: 0, count: 8))
|
||||
data.append(Data(hexString: receiverID) ?? Data(repeating: 0, count: 8))
|
||||
|
||||
data.appendUInt8(packetType)
|
||||
data.appendUInt8(errorCode)
|
||||
data.appendDate(timestamp)
|
||||
data.appendString(reason)
|
||||
return data
|
||||
}
|
||||
|
||||
static func fromBinaryData(_ data: Data) -> ProtocolNack? {
|
||||
let dataCopy = Data(data)
|
||||
guard dataCopy.count >= 52 else { return nil } // Minimum size
|
||||
|
||||
var offset = 0
|
||||
guard let originalPacketID = dataCopy.readUUID(at: &offset),
|
||||
let nackID = dataCopy.readUUID(at: &offset),
|
||||
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
||||
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
||||
let packetType = dataCopy.readUInt8(at: &offset),
|
||||
InputValidator.validateMessageType(packetType),
|
||||
let errorCode = dataCopy.readUInt8(at: &offset),
|
||||
let timestamp = dataCopy.readDate(at: &offset),
|
||||
InputValidator.validateTimestamp(timestamp),
|
||||
let reasonRaw = dataCopy.readString(at: &offset),
|
||||
let reason = InputValidator.validateReasonString(reasonRaw) else { return nil }
|
||||
|
||||
let senderID = senderIDData.hexEncodedString()
|
||||
let receiverID = receiverIDData.hexEncodedString()
|
||||
guard InputValidator.validatePeerID(senderID),
|
||||
InputValidator.validatePeerID(receiverID) else { return nil }
|
||||
|
||||
return ProtocolNack(originalPacketID: originalPacketID,
|
||||
nackID: nackID,
|
||||
senderID: senderID,
|
||||
receiverID: receiverID,
|
||||
packetType: packetType,
|
||||
timestamp: timestamp,
|
||||
reason: reason,
|
||||
errorCode: errorCode)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Peer Identity Rotation
|
||||
|
||||
/// Announces a peer's cryptographic identity to enable secure communication.
|
||||
/// Contains the peer's Noise static public key and supports identity rotation
|
||||
/// by binding ephemeral peer IDs to stable cryptographic fingerprints.
|
||||
/// - Note: Critical for establishing end-to-end encrypted channels
|
||||
struct NoiseIdentityAnnouncement: Codable {
|
||||
let peerID: String // Current ephemeral peer ID
|
||||
let publicKey: Data // Noise static public key
|
||||
let signingPublicKey: Data // Ed25519 signing public key
|
||||
let nickname: String // Current nickname
|
||||
let timestamp: Date // When this binding was created
|
||||
let previousPeerID: String? // Previous peer ID (for smooth transition)
|
||||
let signature: Data // Signature proving ownership
|
||||
|
||||
init(peerID: String, publicKey: Data, signingPublicKey: Data, nickname: String, timestamp: Date, previousPeerID: String? = nil, signature: Data) {
|
||||
self.peerID = peerID
|
||||
self.publicKey = publicKey
|
||||
self.signingPublicKey = signingPublicKey
|
||||
// Trim whitespace from nickname
|
||||
self.nickname = nickname.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.timestamp = timestamp
|
||||
self.previousPeerID = previousPeerID
|
||||
self.signature = signature
|
||||
}
|
||||
|
||||
// Custom decoder to ensure nickname is trimmed
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.peerID = try container.decode(String.self, forKey: .peerID)
|
||||
self.publicKey = try container.decode(Data.self, forKey: .publicKey)
|
||||
self.signingPublicKey = try container.decode(Data.self, forKey: .signingPublicKey)
|
||||
// Trim whitespace from decoded nickname
|
||||
let rawNickname = try container.decode(String.self, forKey: .nickname)
|
||||
self.nickname = rawNickname.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.timestamp = try container.decode(Date.self, forKey: .timestamp)
|
||||
self.previousPeerID = try container.decodeIfPresent(String.self, forKey: .previousPeerID)
|
||||
self.signature = try container.decode(Data.self, forKey: .signature)
|
||||
}
|
||||
|
||||
func encode() -> Data? {
|
||||
return try? JSONEncoder().encode(self)
|
||||
}
|
||||
|
||||
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 InputValidator.validatePeerID(peerID) else { return nil }
|
||||
|
||||
guard let publicKey = dataCopy.readData(at: &offset),
|
||||
InputValidator.validatePublicKey(publicKey),
|
||||
let signingPublicKey = dataCopy.readData(at: &offset),
|
||||
InputValidator.validatePublicKey(signingPublicKey),
|
||||
let rawNickname = dataCopy.readString(at: &offset),
|
||||
let nickname = InputValidator.validateNickname(rawNickname),
|
||||
let timestamp = dataCopy.readDate(at: &offset),
|
||||
InputValidator.validateTimestamp(timestamp) 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 }
|
||||
let prevID = prevIDBytes.hexEncodedString()
|
||||
guard InputValidator.validatePeerID(prevID) else { return nil }
|
||||
previousPeerID = prevID
|
||||
}
|
||||
|
||||
guard let signature = dataCopy.readData(at: &offset),
|
||||
InputValidator.validateSignature(signature) 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
|
||||
struct PeerIdentityBinding {
|
||||
let currentPeerID: String // Current ephemeral ID
|
||||
let fingerprint: String // Permanent cryptographic identity
|
||||
let publicKey: Data // Noise static public key
|
||||
let signingPublicKey: Data // Ed25519 signing public key
|
||||
let nickname: String // Last known nickname
|
||||
let bindingTimestamp: Date // When this binding was created
|
||||
let signature: Data // Cryptographic proof of binding
|
||||
|
||||
// Verify the binding signature
|
||||
func verify() -> Bool {
|
||||
let bindingData = currentPeerID.data(using: .utf8)! + publicKey +
|
||||
String(Int64(bindingTimestamp.timeIntervalSince1970 * 1000)).data(using: .utf8)!
|
||||
|
||||
do {
|
||||
let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingPublicKey)
|
||||
return signingKey.isValidSignature(signature, for: bindingData)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
// PeerIdentityBinding removed (unused).
|
||||
|
||||
|
||||
// MARK: - Delivery Status
|
||||
@@ -1020,13 +460,7 @@ protocol BitchatDelegate: AnyObject {
|
||||
// Optional method to check if a fingerprint belongs to a favorite peer
|
||||
func isFavorite(fingerprint: String) -> Bool
|
||||
|
||||
// Delivery confirmation methods
|
||||
func didReceiveDeliveryAck(_ ack: DeliveryAck)
|
||||
func didReceiveReadReceipt(_ receipt: ReadReceipt)
|
||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
|
||||
|
||||
// Peer availability tracking
|
||||
func peerAvailabilityChanged(_ peerID: String, available: Bool)
|
||||
}
|
||||
|
||||
// Provide default implementation to make it effectively optional
|
||||
@@ -1035,19 +469,41 @@ extension BitchatDelegate {
|
||||
return false
|
||||
}
|
||||
|
||||
func didReceiveDeliveryAck(_ ack: DeliveryAck) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceiveReadReceipt(_ receipt: ReadReceipt) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
||||
// Default empty implementation
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Noise Payload Helpers
|
||||
|
||||
/// Helper to create typed Noise payloads
|
||||
struct NoisePayload {
|
||||
let type: NoisePayloadType
|
||||
let data: Data
|
||||
|
||||
func peerAvailabilityChanged(_ peerID: String, available: Bool) {
|
||||
// Default empty implementation
|
||||
/// Encode payload with type prefix
|
||||
func encode() -> Data {
|
||||
var encoded = Data()
|
||||
encoded.append(type.rawValue)
|
||||
encoded.append(data)
|
||||
return encoded
|
||||
}
|
||||
|
||||
/// Decode payload from data
|
||||
static func decode(_ data: Data) -> NoisePayload? {
|
||||
// Ensure we have at least 1 byte for the type
|
||||
guard !data.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Safely get the first byte
|
||||
let firstByte = data[data.startIndex]
|
||||
guard let type = NoisePayloadType(rawValue: firstByte) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a proper Data copy (not a subsequence) for thread safety
|
||||
let payloadData = data.count > 1 ? Data(data.dropFirst()) : Data()
|
||||
return NoisePayload(type: type, data: payloadData)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user