mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 11:05:19 +00:00
Refactor: Extract types from BitchatProtocol (#611)
* Extract BitchatMessage into a separate file * Convert `fromBinaryPayload` to `convenience init?` * Extract message dedup into an extension * Remove dead `formatMessageContent` * Minor refactor of timestamp and username formatting * Remove dead `getSenderColor` * Extract MessagePadding into a separate file * Extract BitchatPacket into a separate file * Extract ReadReceipt into a separate file * Extract NoisePayload into a separate file * Remove unnecessary import
This commit is contained in:
@@ -59,61 +59,6 @@
|
||||
///
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
// MARK: - Message Padding
|
||||
|
||||
/// Provides privacy-preserving message padding to obscure actual content length.
|
||||
/// Uses PKCS#7-style padding with random bytes to prevent traffic analysis.
|
||||
struct MessagePadding {
|
||||
// Standard block sizes for padding
|
||||
static let blockSizes = [256, 512, 1024, 2048]
|
||||
|
||||
// Add PKCS#7-style padding to reach target size
|
||||
static func pad(_ data: Data, toSize targetSize: Int) -> Data {
|
||||
guard data.count < targetSize else { return data }
|
||||
|
||||
let paddingNeeded = targetSize - data.count
|
||||
// Constrain to 255 to fit a single-byte pad length marker
|
||||
guard paddingNeeded > 0 && paddingNeeded <= 255 else { return data }
|
||||
|
||||
var padded = data
|
||||
// PKCS#7: All pad bytes are equal to the pad length
|
||||
padded.append(contentsOf: Array(repeating: UInt8(paddingNeeded), count: paddingNeeded))
|
||||
return padded
|
||||
}
|
||||
|
||||
// Remove padding from data
|
||||
static func unpad(_ data: Data) -> Data {
|
||||
guard !data.isEmpty else { return data }
|
||||
let last = data.last!
|
||||
let paddingLength = Int(last)
|
||||
// Must have at least 1 pad byte and not exceed data length
|
||||
guard paddingLength > 0 && paddingLength <= data.count else { return data }
|
||||
// Verify PKCS#7: all last N bytes equal to pad length
|
||||
let start = data.count - paddingLength
|
||||
let tail = data[start...]
|
||||
for b in tail { if b != last { return data } }
|
||||
return Data(data[..<start])
|
||||
}
|
||||
|
||||
// Find optimal block size for data
|
||||
static func optimalBlockSize(for dataSize: Int) -> Int {
|
||||
// Account for encryption overhead (~16 bytes for AES-GCM tag)
|
||||
let totalSize = dataSize + 16
|
||||
|
||||
// Find smallest block that fits
|
||||
for blockSize in blockSizes {
|
||||
if totalSize <= blockSize {
|
||||
return blockSize
|
||||
}
|
||||
}
|
||||
|
||||
// For very large messages, just use the original size
|
||||
// (will be fragmented anyway)
|
||||
return dataSize
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Message Types
|
||||
|
||||
@@ -183,187 +128,6 @@ enum LazyHandshakeState {
|
||||
case failed(Error) // Handshake failed
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// MARK: - Core Protocol Structures
|
||||
|
||||
/// The core packet structure for all BitChat protocol messages.
|
||||
/// Encapsulates all data needed for routing through the mesh network,
|
||||
/// including TTL for hop limiting and optional encryption.
|
||||
/// - Note: Packets larger than BLE MTU (512 bytes) are automatically fragmented
|
||||
struct BitchatPacket: Codable {
|
||||
let version: UInt8
|
||||
let type: UInt8
|
||||
let senderID: Data
|
||||
let recipientID: Data?
|
||||
let timestamp: UInt64
|
||||
let payload: Data
|
||||
var signature: Data?
|
||||
var ttl: UInt8
|
||||
|
||||
init(type: UInt8, senderID: Data, recipientID: Data?, timestamp: UInt64, payload: Data, signature: Data?, ttl: UInt8) {
|
||||
self.version = 1
|
||||
self.type = type
|
||||
self.senderID = senderID
|
||||
self.recipientID = recipientID
|
||||
self.timestamp = timestamp
|
||||
self.payload = payload
|
||||
self.signature = signature
|
||||
self.ttl = ttl
|
||||
}
|
||||
|
||||
// Convenience initializer for new binary format
|
||||
init(type: UInt8, ttl: UInt8, senderID: String, payload: Data) {
|
||||
self.version = 1
|
||||
self.type = type
|
||||
// Convert hex string peer ID to binary data (8 bytes)
|
||||
var senderData = Data()
|
||||
var tempID = senderID
|
||||
while tempID.count >= 2 {
|
||||
let hexByte = String(tempID.prefix(2))
|
||||
if let byte = UInt8(hexByte, radix: 16) {
|
||||
senderData.append(byte)
|
||||
}
|
||||
tempID = String(tempID.dropFirst(2))
|
||||
}
|
||||
self.senderID = senderData
|
||||
self.recipientID = nil
|
||||
self.timestamp = UInt64(Date().timeIntervalSince1970 * 1000) // milliseconds
|
||||
self.payload = payload
|
||||
self.signature = nil
|
||||
self.ttl = ttl
|
||||
}
|
||||
|
||||
var data: Data? {
|
||||
BinaryProtocol.encode(self)
|
||||
}
|
||||
|
||||
func toBinaryData(padding: Bool = true) -> Data? {
|
||||
BinaryProtocol.encode(self, padding: padding)
|
||||
}
|
||||
|
||||
// Backward-compatible helper (defaults to padded encoding)
|
||||
func toBinaryData() -> Data? {
|
||||
toBinaryData(padding: true)
|
||||
}
|
||||
|
||||
/// Create binary representation for signing (without signature and TTL fields)
|
||||
/// TTL is excluded because it changes during packet relay operations
|
||||
func toBinaryDataForSigning() -> Data? {
|
||||
// Create a copy without signature and with fixed TTL for signing
|
||||
// TTL must be excluded because it changes during relay
|
||||
let unsignedPacket = BitchatPacket(
|
||||
type: type,
|
||||
senderID: senderID,
|
||||
recipientID: recipientID,
|
||||
timestamp: timestamp,
|
||||
payload: payload,
|
||||
signature: nil, // Remove signature for signing
|
||||
ttl: 0 // Use fixed TTL=0 for signing to ensure relay compatibility
|
||||
)
|
||||
return BinaryProtocol.encode(unsignedPacket)
|
||||
}
|
||||
|
||||
static func from(_ data: Data) -> BitchatPacket? {
|
||||
BinaryProtocol.decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// MARK: - Read Receipts
|
||||
|
||||
// Read receipt structure
|
||||
struct ReadReceipt: Codable {
|
||||
let originalMessageID: String
|
||||
let receiptID: String
|
||||
var readerID: String // Who read it
|
||||
let readerNickname: String
|
||||
let timestamp: Date
|
||||
|
||||
init(originalMessageID: String, readerID: String, readerNickname: String) {
|
||||
self.originalMessageID = originalMessageID
|
||||
self.receiptID = UUID().uuidString
|
||||
self.readerID = readerID
|
||||
self.readerNickname = readerNickname
|
||||
self.timestamp = Date()
|
||||
}
|
||||
|
||||
// For binary decoding
|
||||
private init(originalMessageID: String, receiptID: String, readerID: String, readerNickname: String, timestamp: Date) {
|
||||
self.originalMessageID = originalMessageID
|
||||
self.receiptID = receiptID
|
||||
self.readerID = readerID
|
||||
self.readerNickname = readerNickname
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
func encode() -> Data? {
|
||||
try? JSONEncoder().encode(self)
|
||||
}
|
||||
|
||||
static func decode(from data: Data) -> ReadReceipt? {
|
||||
try? JSONDecoder().decode(ReadReceipt.self, from: data)
|
||||
}
|
||||
|
||||
// MARK: - Binary Encoding
|
||||
|
||||
func toBinaryData() -> Data {
|
||||
var data = Data()
|
||||
data.appendUUID(originalMessageID)
|
||||
data.appendUUID(receiptID)
|
||||
// ReaderID as 8-byte hex string
|
||||
var readerData = Data()
|
||||
var tempID = readerID
|
||||
while tempID.count >= 2 && readerData.count < 8 {
|
||||
let hexByte = String(tempID.prefix(2))
|
||||
if let byte = UInt8(hexByte, radix: 16) {
|
||||
readerData.append(byte)
|
||||
}
|
||||
tempID = String(tempID.dropFirst(2))
|
||||
}
|
||||
while readerData.count < 8 {
|
||||
readerData.append(0)
|
||||
}
|
||||
data.append(readerData)
|
||||
data.appendDate(timestamp)
|
||||
data.appendString(readerNickname)
|
||||
return data
|
||||
}
|
||||
|
||||
static func fromBinaryData(_ data: Data) -> ReadReceipt? {
|
||||
// Create defensive copy
|
||||
let dataCopy = Data(data)
|
||||
|
||||
// Minimum size: 2 UUIDs (32) + readerID (8) + timestamp (8) + min nickname
|
||||
guard dataCopy.count >= 49 else { return nil }
|
||||
|
||||
var offset = 0
|
||||
|
||||
guard let originalMessageID = dataCopy.readUUID(at: &offset),
|
||||
let receiptID = dataCopy.readUUID(at: &offset) else { return nil }
|
||||
|
||||
guard let readerIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||
let readerID = readerIDData.hexEncodedString()
|
||||
guard InputValidator.validatePeerID(readerID) else { return nil }
|
||||
|
||||
guard let timestamp = dataCopy.readDate(at: &offset),
|
||||
InputValidator.validateTimestamp(timestamp),
|
||||
let readerNicknameRaw = dataCopy.readString(at: &offset),
|
||||
let readerNickname = InputValidator.validateNickname(readerNicknameRaw) else { return nil }
|
||||
|
||||
return ReadReceipt(originalMessageID: originalMessageID,
|
||||
receiptID: receiptID,
|
||||
readerID: readerID,
|
||||
readerNickname: readerNickname,
|
||||
timestamp: timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
|
||||
|
||||
// MARK: - Delivery Status
|
||||
|
||||
// Delivery status for messages
|
||||
@@ -429,37 +193,3 @@ extension BitchatDelegate {
|
||||
// Default empty implementation
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Noise Payload Helpers
|
||||
|
||||
/// Helper to create typed Noise payloads
|
||||
struct NoisePayload {
|
||||
let type: NoisePayloadType
|
||||
let data: Data
|
||||
|
||||
/// 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