// // BitchatProtocol.swift // bitchat // // This is free and unencumbered software released into the public domain. // For more information, see // import Foundation import CryptoKit // Privacy-preserving padding utilities 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 // PKCS#7 only supports padding up to 255 bytes // If we need more padding than that, don't pad - return original data guard paddingNeeded <= 255 else { return data } var padded = data // Standard PKCS#7 padding var randomBytes = [UInt8](repeating: 0, count: paddingNeeded - 1) _ = SecRandomCopyBytes(kSecRandomDefault, paddingNeeded - 1, &randomBytes) padded.append(contentsOf: randomBytes) padded.append(UInt8(paddingNeeded)) return padded } // Remove padding from data static func unpad(_ data: Data) -> Data { guard !data.isEmpty else { return data } // Last byte tells us how much padding to remove let paddingLength = Int(data[data.count - 1]) guard paddingLength > 0 && paddingLength <= data.count else { // Debug logging for 243-byte packets if data.count == 243 { } return data } let result = data.prefix(data.count - paddingLength) // Debug logging for 243-byte packets if data.count == 243 { } return result } // 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 } } 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 channelAnnounce = 0x08 // Announce password-protected channel status case deliveryAck = 0x0A // Acknowledge message received case deliveryStatusRequest = 0x0B // Request delivery status update case readReceipt = 0x0C // Message has been read/viewed // 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 case channelKeyVerifyRequest = 0x14 // Request key verification for a channel case channelKeyVerifyResponse = 0x15 // Response to key verification request case channelPasswordUpdate = 0x16 // Distribute new password to channel members case channelMetadata = 0x17 // Announce channel creator and metadata // Protocol version negotiation case versionHello = 0x20 // Initial version announcement case versionAck = 0x21 // Version acknowledgment 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 .channelAnnounce: return "channelAnnounce" case .deliveryAck: return "deliveryAck" case .deliveryStatusRequest: return "deliveryStatusRequest" case .readReceipt: return "readReceipt" case .noiseHandshakeInit: return "noiseHandshakeInit" case .noiseHandshakeResp: return "noiseHandshakeResp" case .noiseEncrypted: return "noiseEncrypted" case .noiseIdentityAnnounce: return "noiseIdentityAnnounce" case .channelKeyVerifyRequest: return "channelKeyVerifyRequest" case .channelKeyVerifyResponse: return "channelKeyVerifyResponse" case .channelPasswordUpdate: return "channelPasswordUpdate" case .channelMetadata: return "channelMetadata" case .versionHello: return "versionHello" case .versionAck: return "versionAck" } } } // Special recipient ID for broadcast messages struct SpecialRecipients { static let broadcast = Data(repeating: 0xFF, count: 8) // All 0xFF = broadcast } struct BitchatPacket: Codable { let version: UInt8 let type: UInt8 let senderID: Data let recipientID: Data? let timestamp: UInt64 let payload: Data let 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() -> Data? { BinaryProtocol.encode(self) } static func from(_ data: Data) -> BitchatPacket? { BinaryProtocol.decode(data) } } // Delivery acknowledgment structure 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? { // Minimum size: 2 UUIDs (32) + recipientID (8) + hopCount (1) + timestamp (8) + min nickname guard data.count >= 50 else { return nil } var offset = 0 guard let originalMessageID = data.readUUID(at: &offset), let ackID = data.readUUID(at: &offset) else { return nil } guard offset + 8 <= data.count else { return nil } let recipientIDData = data[offset.. 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? { // Minimum size: 2 UUIDs (32) + readerID (8) + timestamp (8) + min nickname guard data.count >= 49 else { return nil } var offset = 0 guard let originalMessageID = data.readUUID(at: &offset), let receiptID = data.readUUID(at: &offset) else { return nil } guard offset + 8 <= data.count else { return nil } let readerIDData = data[offset.. Data? { return try? JSONEncoder().encode(self) } static func decode(from data: Data) -> ChannelKeyVerifyRequest? { try? JSONDecoder().decode(ChannelKeyVerifyRequest.self, from: data) } // MARK: - Binary Encoding func toBinaryData() -> Data { var data = Data() data.appendString(channel) // RequesterID as 8-byte hex string var requesterData = Data() var tempID = requesterID while tempID.count >= 2 && requesterData.count < 8 { let hexByte = String(tempID.prefix(2)) if let byte = UInt8(hexByte, radix: 16) { requesterData.append(byte) } tempID = String(tempID.dropFirst(2)) } while requesterData.count < 8 { requesterData.append(0) } data.append(requesterData) data.appendString(keyCommitment) data.appendDate(timestamp) return data } static func fromBinaryData(_ data: Data) -> ChannelKeyVerifyRequest? { var offset = 0 guard let channel = data.readString(at: &offset) else { return nil } guard offset + 8 <= data.count else { return nil } let requesterIDData = data[offset.. Data? { return try? JSONEncoder().encode(self) } static func decode(from data: Data) -> ChannelKeyVerifyResponse? { try? JSONDecoder().decode(ChannelKeyVerifyResponse.self, from: data) } // MARK: - Binary Encoding func toBinaryData() -> Data { var data = Data() data.appendString(channel) // ResponderID as 8-byte hex string var responderData = Data() var tempID = responderID while tempID.count >= 2 && responderData.count < 8 { let hexByte = String(tempID.prefix(2)) if let byte = UInt8(hexByte, radix: 16) { responderData.append(byte) } tempID = String(tempID.dropFirst(2)) } while responderData.count < 8 { responderData.append(0) } data.append(responderData) data.appendUInt8(verified ? 1 : 0) data.appendDate(timestamp) return data } static func fromBinaryData(_ data: Data) -> ChannelKeyVerifyResponse? { var offset = 0 guard let channel = data.readString(at: &offset) else { return nil } guard offset + 8 <= data.count else { return nil } let responderIDData = data[offset.. Data? { return try? JSONEncoder().encode(self) } static func decode(from data: Data) -> ChannelPasswordUpdate? { try? JSONDecoder().decode(ChannelPasswordUpdate.self, from: data) } // MARK: - Binary Encoding func toBinaryData() -> Data { var data = Data() data.appendString(channel) // OwnerID as 8-byte hex string var ownerData = Data() var tempID = ownerID while tempID.count >= 2 && ownerData.count < 8 { let hexByte = String(tempID.prefix(2)) if let byte = UInt8(hexByte, radix: 16) { ownerData.append(byte) } tempID = String(tempID.dropFirst(2)) } while ownerData.count < 8 { ownerData.append(0) } data.append(ownerData) data.appendString(ownerFingerprint) data.appendData(encryptedPassword) data.appendString(newKeyCommitment) data.appendDate(timestamp) return data } static func fromBinaryData(_ data: Data) -> ChannelPasswordUpdate? { var offset = 0 guard let channel = data.readString(at: &offset) else { return nil } guard offset + 8 <= data.count else { return nil } let ownerIDData = data[offset.. Data? { return try? JSONEncoder().encode(self) } static func decode(from data: Data) -> ChannelMetadata? { try? JSONDecoder().decode(ChannelMetadata.self, from: data) } // MARK: - Binary Encoding func toBinaryData() -> Data { var data = Data() // Flags byte: bit 0 = hasKeyCommitment var flags: UInt8 = 0 if keyCommitment != nil { flags |= 0x01 } data.appendUInt8(flags) data.appendString(channel) // CreatorID as 8-byte hex string var creatorData = Data() var tempID = creatorID while tempID.count >= 2 && creatorData.count < 8 { let hexByte = String(tempID.prefix(2)) if let byte = UInt8(hexByte, radix: 16) { creatorData.append(byte) } tempID = String(tempID.dropFirst(2)) } while creatorData.count < 8 { creatorData.append(0) } data.append(creatorData) data.appendString(creatorFingerprint) data.appendDate(createdAt) data.appendUInt8(isPasswordProtected ? 1 : 0) if let keyCommitment = keyCommitment { data.appendString(keyCommitment) } return data } static func fromBinaryData(_ data: Data) -> ChannelMetadata? { var offset = 0 guard let flags = data.readUInt8(at: &offset) else { return nil } let hasKeyCommitment = (flags & 0x01) != 0 guard let channel = data.readString(at: &offset) else { return nil } guard offset + 8 <= data.count else { return nil } let creatorIDData = data[offset.. 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? { // Minimum size check guard data.count >= 20 else { return nil } var offset = 0 guard let flags = data.readUInt8(at: &offset) else { return nil } let hasPreviousPeerID = (flags & 0x01) != 0 guard offset + 8 <= data.count else { return nil } let peerIDData = data[offset.. 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 } } } // MARK: - Protocol Version Negotiation // Protocol version constants struct ProtocolVersion { static let current: UInt8 = 1 static let minimum: UInt8 = 1 static let maximum: UInt8 = 1 // Future versions can be added here static let supportedVersions: Set = [1] static func isSupported(_ version: UInt8) -> Bool { return supportedVersions.contains(version) } static func negotiateVersion(clientVersions: [UInt8], serverVersions: [UInt8]) -> UInt8? { // Find the highest common version let clientSet = Set(clientVersions) let serverSet = Set(serverVersions) let common = clientSet.intersection(serverSet) return common.max() } } // Version negotiation hello message struct VersionHello: Codable { let supportedVersions: [UInt8] // List of supported protocol versions let preferredVersion: UInt8 // Preferred version (usually the latest) let clientVersion: String // App version string (e.g., "1.0.0") let platform: String // Platform identifier (e.g., "iOS", "macOS") let capabilities: [String]? // Optional capability flags for future extensions init(supportedVersions: [UInt8] = Array(ProtocolVersion.supportedVersions), preferredVersion: UInt8 = ProtocolVersion.current, clientVersion: String, platform: String, capabilities: [String]? = nil) { self.supportedVersions = supportedVersions self.preferredVersion = preferredVersion self.clientVersion = clientVersion self.platform = platform self.capabilities = capabilities } func encode() -> Data? { return try? JSONEncoder().encode(self) } static func decode(from data: Data) -> VersionHello? { try? JSONDecoder().decode(VersionHello.self, from: data) } // MARK: - Binary Encoding func toBinaryData() -> Data { var data = Data() // Flags byte: bit 0 = hasCapabilities var flags: UInt8 = 0 if capabilities != nil { flags |= 0x01 } data.appendUInt8(flags) // Supported versions array data.appendUInt8(UInt8(supportedVersions.count)) for version in supportedVersions { data.appendUInt8(version) } data.appendUInt8(preferredVersion) data.appendString(clientVersion) data.appendString(platform) if let capabilities = capabilities { data.appendUInt8(UInt8(capabilities.count)) for capability in capabilities { data.appendString(capability) } } return data } static func fromBinaryData(_ data: Data) -> VersionHello? { // Minimum size check: flags(1) + versionCount(1) + at least one version(1) + preferredVersion(1) + min strings guard data.count >= 4 else { return nil } var offset = 0 guard let flags = data.readUInt8(at: &offset) else { return nil } let hasCapabilities = (flags & 0x01) != 0 guard let versionCount = data.readUInt8(at: &offset) else { return nil } var supportedVersions: [UInt8] = [] for _ in 0.. Data? { return try? JSONEncoder().encode(self) } static func decode(from data: Data) -> VersionAck? { try? JSONDecoder().decode(VersionAck.self, from: data) } // MARK: - Binary Encoding func toBinaryData() -> Data { var data = Data() // Flags byte: bit 0 = hasCapabilities, bit 1 = hasReason var flags: UInt8 = 0 if capabilities != nil { flags |= 0x01 } if reason != nil { flags |= 0x02 } data.appendUInt8(flags) data.appendUInt8(agreedVersion) data.appendString(serverVersion) data.appendString(platform) data.appendUInt8(rejected ? 1 : 0) if let capabilities = capabilities { data.appendUInt8(UInt8(capabilities.count)) for capability in capabilities { data.appendString(capability) } } if let reason = reason { data.appendString(reason) } return data } static func fromBinaryData(_ data: Data) -> VersionAck? { // Minimum size: flags(1) + version(1) + rejected(1) + min strings guard data.count >= 5 else { return nil } var offset = 0 guard let flags = data.readUInt8(at: &offset) else { return nil } let hasCapabilities = (flags & 0x01) != 0 let hasReason = (flags & 0x02) != 0 guard let agreedVersion = data.readUInt8(at: &offset), let serverVersion = data.readString(at: &offset), let platform = data.readString(at: &offset), let rejectedByte = data.readUInt8(at: &offset) else { return nil } let rejected = rejectedByte != 0 var capabilities: [String]? = nil if hasCapabilities { guard let capCount = data.readUInt8(at: &offset) else { return nil } capabilities = [] for _ in 0.. String? // 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) // Channel key verification methods func didReceiveChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, from peerID: String) func didReceiveChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, from peerID: String) func didReceiveChannelPasswordUpdate(_ update: ChannelPasswordUpdate, from peerID: String) // Channel metadata methods func didReceiveChannelMetadata(_ metadata: ChannelMetadata, from peerID: String) } // Provide default implementation to make it effectively optional extension BitchatDelegate { func isFavorite(fingerprint: String) -> Bool { return false } func didReceiveChannelLeave(_ channel: String, from peerID: String) { // Default empty implementation } func didReceivePasswordProtectedChannelAnnouncement(_ channel: String, isProtected: Bool, creatorID: String?, keyCommitment: String?) { // Default empty implementation } func didReceiveChannelRetentionAnnouncement(_ channel: String, enabled: Bool, creatorID: String?) { // Default empty implementation } func decryptChannelMessage(_ encryptedContent: Data, channel: String) -> String? { // Default returns nil (unable to decrypt) return nil } func didReceiveDeliveryAck(_ ack: DeliveryAck) { // Default empty implementation } func didReceiveReadReceipt(_ receipt: ReadReceipt) { // Default empty implementation } func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) { // Default empty implementation } func didReceiveChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, from peerID: String) { // Default empty implementation } func didReceiveChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, from peerID: String) { // Default empty implementation } func didReceiveChannelPasswordUpdate(_ update: ChannelPasswordUpdate, from peerID: String) { // Default empty implementation } func didReceiveChannelMetadata(_ metadata: ChannelMetadata, from peerID: String) { // Default empty implementation } }