mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 06:25:20 +00:00
BLE stays the control plane: a sender with a queued file > 64 KiB to a directly connected peer advertising the wifiBulk capability sends a bulkTransferOffer (0x04) inside the established Noise session — transferID, file size, payload SHA-256, a fresh 32-byte token, and a random per-transfer Bonjour instance name. The receiver answers bulkTransferResponse (0x05) with its own token half, then both sides meet on a per-transfer _bitchat-bulk._tcp channel over peer-to-peer Wi-Fi (AWDL). The TCP stream is secured independently of TLS: both tokens traveled inside Noise, so only the two peers can derive the ChaChaPoly channel key (HKDF-SHA256, domain "bitchat-bulk-v1", transferID as salt). Frames are length-prefixed sealed boxes with structured direction+counter nonces; the first frame must prove knowledge of the key or the client is disconnected, and the final hash is verified against the offer before delivery. Decline, timeout, or any mid-transfer error falls back to BLE fragmentation exactly once, driving the same TransferProgressManager stream so the UI is unchanged. Wi-Fi-negotiated transfers may carry up to 8 MiB (new FileTransferLimits.maxWifiBulkPayloadBytes, enforced by the receiver from the accepted offer); the BLE path keeps its existing caps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
77 lines
2.6 KiB
Swift
77 lines
2.6 KiB
Swift
import BitFoundation
|
|
import Foundation
|
|
|
|
struct BLEFileTransferDeliveryPlan: Equatable {
|
|
let isPrivateMessage: Bool
|
|
let shouldTrackForSync: Bool
|
|
}
|
|
|
|
enum BLEFileTransferPolicy {
|
|
static func isSelfEcho(packet: BitchatPacket, from peerID: PeerID, localPeerID: PeerID) -> Bool {
|
|
peerID == localPeerID && packet.ttl != 0
|
|
}
|
|
|
|
static func deliveryPlan(packet: BitchatPacket, localPeerID: PeerID) -> BLEFileTransferDeliveryPlan? {
|
|
guard let recipientID = packet.recipientID else {
|
|
return BLEFileTransferDeliveryPlan(isPrivateMessage: false, shouldTrackForSync: true)
|
|
}
|
|
|
|
let isBroadcast = recipientID.allSatisfy { $0 == 0xFF }
|
|
if isBroadcast {
|
|
return BLEFileTransferDeliveryPlan(isPrivateMessage: false, shouldTrackForSync: true)
|
|
}
|
|
|
|
guard PeerID(hexData: recipientID) == localPeerID else {
|
|
return nil
|
|
}
|
|
|
|
return BLEFileTransferDeliveryPlan(isPrivateMessage: true, shouldTrackForSync: false)
|
|
}
|
|
}
|
|
|
|
struct BLEIncomingFileAcceptance {
|
|
let filePacket: BitchatFilePacket
|
|
let mime: MimeType
|
|
}
|
|
|
|
enum BLEIncomingFileRejection: Error, Equatable {
|
|
case malformedPayload
|
|
case payloadTooLarge(bytes: Int)
|
|
case unsupportedMime(mimeType: String?, bytes: Int)
|
|
case magicMismatch(mime: MimeType, bytes: Int, prefixHex: String)
|
|
}
|
|
|
|
enum BLEIncomingFileValidator {
|
|
/// `limit` defaults to the Bluetooth payload cap; Wi-Fi bulk deliveries
|
|
/// pass the ceiling enforced against the accepted offer.
|
|
static func validate(
|
|
payload: Data,
|
|
limit: Int = FileTransferLimits.maxPayloadBytes
|
|
) -> Result<BLEIncomingFileAcceptance, BLEIncomingFileRejection> {
|
|
guard let filePacket = BitchatFilePacket.decode(payload, limit: limit) else {
|
|
return .failure(.malformedPayload)
|
|
}
|
|
|
|
guard FileTransferLimits.isValidPayload(filePacket.content.count, limit: limit) else {
|
|
return .failure(.payloadTooLarge(bytes: filePacket.content.count))
|
|
}
|
|
|
|
guard let mime = MimeType(filePacket.mimeType), mime.isAllowed else {
|
|
return .failure(.unsupportedMime(
|
|
mimeType: filePacket.mimeType,
|
|
bytes: filePacket.content.count
|
|
))
|
|
}
|
|
|
|
guard mime.matches(data: filePacket.content) else {
|
|
return .failure(.magicMismatch(
|
|
mime: mime,
|
|
bytes: filePacket.content.count,
|
|
prefixHex: filePacket.content.prefix(20).map { String(format: "%02x", $0) }.joined(separator: " ")
|
|
))
|
|
}
|
|
|
|
return .success(BLEIncomingFileAcceptance(filePacket: filePacket, mime: mime))
|
|
}
|
|
}
|