Wi-Fi bulk transport: AWDL data plane for large media with BLE fallback

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>
This commit is contained in:
jack
2026-07-06 20:35:40 +02:00
co-authored by Claude Fable 5
parent 688b954fb8
commit e4b3cff5fa
21 changed files with 2614 additions and 21 deletions
@@ -2,6 +2,10 @@
public enum FileTransferLimits {
/// Absolute ceiling enforced for any file payload (voice, image, other).
public static let maxPayloadBytes: Int = 1 * 1024 * 1024 // 1 MiB
/// Ceiling for transfers negotiated onto the peer-to-peer Wi-Fi bulk
/// channel. The receiver enforces it against the size in the accepted
/// offer; the Bluetooth path keeps `maxPayloadBytes`.
public static let maxWifiBulkPayloadBytes: Int = 8 * 1024 * 1024 // 8 MiB
/// Voice notes stay small for low-latency relays.
public static let maxVoiceNoteBytes: Int = 512 * 1024 // 512 KiB
/// Compressed images after downscaling should comfortably fit under this budget.
@@ -17,7 +21,7 @@ public enum FileTransferLimits {
return maxPayloadBytes + tlvEnvelopeOverhead + binaryEnvelopeOverhead
}()
public static func isValidPayload(_ size: Int) -> Bool {
size <= maxPayloadBytes
public static func isValidPayload(_ size: Int, limit: Int = maxPayloadBytes) -> Bool {
size <= limit
}
}