mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 23:05:20 +00:00
192 lines
7.9 KiB
Swift
192 lines
7.9 KiB
Swift
//
|
|
// BitchatProtocol.swift
|
|
// bitchat
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
///
|
|
/// # BitchatProtocol
|
|
///
|
|
/// Defines the application-layer protocol for BitChat mesh networking, including
|
|
/// message types, packet structures, and encoding/decoding logic.
|
|
///
|
|
/// ## Overview
|
|
/// BitchatProtocol implements a binary protocol optimized for Bluetooth LE's
|
|
/// constrained bandwidth and MTU limitations. It provides:
|
|
/// - Efficient binary message encoding
|
|
/// - Message fragmentation for large payloads
|
|
/// - TTL-based routing for mesh networks
|
|
/// - Privacy features: message padding and randomized relay jitter
|
|
/// - Integration points for end-to-end encryption
|
|
///
|
|
/// ## Protocol Design
|
|
/// The protocol uses a compact binary format to minimize overhead:
|
|
/// - 1-byte message type identifier
|
|
/// - Variable-length fields with length prefixes
|
|
/// - Network byte order (big-endian) for multi-byte values
|
|
/// - PKCS#7-style padding for privacy
|
|
///
|
|
/// ## Message Flow
|
|
/// 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 BLEService
|
|
/// 5. **Routing**: Relayed by intermediate nodes (TTL decrements)
|
|
/// 6. **Reassembly**: Fragments collected and reassembled
|
|
/// 7. **Decoding**: Binary data parsed back to message objects
|
|
///
|
|
/// ## Security Considerations
|
|
/// - Message padding (to 256/512/1024/2048-byte blocks) obscures actual content length
|
|
/// - Randomized relay jitter reduces the traffic-analysis signal; there is no
|
|
/// cover traffic or per-message timing obfuscation
|
|
/// - Integration with Noise Protocol for E2E encryption
|
|
/// - No persistent identifiers in protocol headers
|
|
///
|
|
/// ## Message Types
|
|
/// - **Announce/Leave**: Peer presence notifications
|
|
/// - **Message**: Public chat messages
|
|
/// - **Fragment**: Multi-part message handling
|
|
/// - **NoiseHandshake/NoiseEncrypted**: Encrypted channel establishment and
|
|
/// all private payloads (messages, delivery acks, read receipts)
|
|
/// - **CourierEnvelope**: Sealed store-and-forward mail
|
|
/// - **RequestSync/FileTransfer**: Gossip history sync and media transfer
|
|
///
|
|
/// ## Future Extensions
|
|
/// The protocol is designed to be extensible:
|
|
/// - Reserved message type ranges for future use
|
|
/// - Version field for protocol evolution
|
|
/// - Optional fields for new features
|
|
///
|
|
|
|
import Foundation
|
|
import CoreBluetooth
|
|
import BitFoundation
|
|
|
|
// 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
|
|
// Private groups (0x04/0x05 reserved by other features)
|
|
case groupInvite = 0x06 // Creator-signed group state (invite)
|
|
case groupKeyUpdate = 0x07 // Creator-signed group state (key rotation / roster update)
|
|
// Live voice (push-to-talk)
|
|
case voiceFrame = 0x08 // One live voice-burst packet (see VoiceBurstPacket)
|
|
// Finalized private media. `0x20` is the value already deployed by the
|
|
// Android client. The complete BitchatFilePacket is encrypted inside
|
|
// Noise before the outer noiseEncrypted packet is fragmented.
|
|
case privateFile = 0x20
|
|
// Versioned peer state authenticated by the surrounding Noise session.
|
|
// This is intentionally distinct from the public announce: announce
|
|
// capabilities are discovery hints, while this payload proves possession
|
|
// of the advertised Noise static key before downgrade state is pinned.
|
|
case authenticatedPeerState = 0x21
|
|
// Verification (QR-based OOB binding)
|
|
case verifyChallenge = 0x10 // Verification challenge
|
|
case verifyResponse = 0x11 // Verification response
|
|
// Transitive verification (web of trust)
|
|
case vouch = 0x12 // Batch of vouch attestations
|
|
|
|
/// #1434 briefly used 0x09 before release. Accept it while prerelease
|
|
/// builds age out, but never emit it. Decoders canonicalize both values to
|
|
/// `.privateFile` so the compatibility alias cannot leak into app logic.
|
|
static let prereleasePrivateFileRawValue: UInt8 = 0x09
|
|
|
|
static func decoded(rawValue: UInt8) -> NoisePayloadType? {
|
|
rawValue == prereleasePrivateFileRawValue ? .privateFile : Self(rawValue: rawValue)
|
|
}
|
|
|
|
static func isPrivateFile(rawValue: UInt8?) -> Bool {
|
|
guard let rawValue else { return false }
|
|
return rawValue == privateFile.rawValue || rawValue == prereleasePrivateFileRawValue
|
|
}
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .privateMessage: return "privateMessage"
|
|
case .readReceipt: return "readReceipt"
|
|
case .delivered: return "delivered"
|
|
case .groupInvite: return "groupInvite"
|
|
case .groupKeyUpdate: return "groupKeyUpdate"
|
|
case .voiceFrame: return "voiceFrame"
|
|
case .privateFile: return "privateFile"
|
|
case .authenticatedPeerState: return "authenticatedPeerState"
|
|
case .verifyChallenge: return "verifyChallenge"
|
|
case .verifyResponse: return "verifyResponse"
|
|
case .vouch: return "vouch"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Handshake State
|
|
|
|
// Lazy handshake state tracking
|
|
enum LazyHandshakeState {
|
|
case none // No session, no handshake attempted
|
|
case handshakeQueued // User action requires handshake
|
|
case handshaking // Currently in handshake process
|
|
case established // Session ready for use
|
|
case failed(Error) // Handshake failed
|
|
}
|
|
|
|
// MARK: - Delegate Protocol
|
|
|
|
protocol BitchatDelegate: AnyObject {
|
|
func didReceiveMessage(_ message: BitchatMessage)
|
|
func didConnectToPeer(_ peerID: PeerID)
|
|
func didDisconnectFromPeer(_ peerID: PeerID)
|
|
func didUpdatePeerList(_ peers: [PeerID])
|
|
|
|
// Optional method to check if a fingerprint belongs to a favorite peer
|
|
func isFavorite(fingerprint: String) -> Bool
|
|
|
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
|
|
|
|
// Low-level events for better separation of concerns
|
|
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date)
|
|
|
|
// Encrypted group broadcast (opaque envelope; decrypted by the group coordinator)
|
|
func didReceiveGroupMessage(payload: Data, timestamp: Date)
|
|
|
|
// Public live-voice burst packet (signature-verified by the transport)
|
|
func didReceivePublicVoiceFrame(from peerID: PeerID, nickname: String, payload: Data, timestamp: Date)
|
|
|
|
// Bluetooth state updates for user notifications
|
|
func didUpdateBluetoothState(_ state: CBManagerState)
|
|
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?)
|
|
}
|
|
|
|
// Provide default implementation to make it effectively optional
|
|
extension BitchatDelegate {
|
|
func isFavorite(fingerprint: String) -> Bool {
|
|
return false
|
|
}
|
|
|
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
|
// Default empty implementation
|
|
}
|
|
|
|
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
|
// Default empty implementation
|
|
}
|
|
|
|
func didReceiveGroupMessage(payload: Data, timestamp: Date) {
|
|
// Default empty implementation
|
|
}
|
|
|
|
func didReceivePublicVoiceFrame(from peerID: PeerID, nickname: String, payload: Data, timestamp: Date) {
|
|
// Default empty implementation
|
|
}
|
|
|
|
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {
|
|
// Default empty implementation
|
|
}
|
|
}
|