How Private Media Is Stored and Transferred in BitChat: A Deep Dive into BLE Mesh File Protocols
BitChat stores private media as TLV-encoded BitchatFilePacket structures, encrypts them via Noise protocol (payload type 0x20), and transports them over Bluetooth Low Energy using policy-aware admission control with automatic fallback handling.
BitChat's approach to private media—voice notes, images, and generic files—differs fundamentally from plaintext messaging. Every file becomes a structured binary payload designed for interoperability between iOS and Android while maintaining end-to-end encryption over a BLE mesh network. This article examines the complete storage and transfer pipeline as implemented in the permissionlesstech/bitchat repository.
The BitchatFilePacket Structure
At the core of BitChat's private media system is BitchatFilePacket, defined in [Bitchat/Protocols/BitchatFilePacket.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/BitchatFilePacket.swift). This struct implements a Type-Length-Value (TLV) wire format that mirrors the Android client's binary protocol, ensuring seamless cross-platform file exchange.
TLV Field Layout
| Tag | Field | Encoding |
|---|---|---|
0x01 |
File name (UTF-8, max 65535 bytes) | 2-byte length prefix + bytes |
0x02 |
File size | 4-byte length (0x04) + UInt32/UInt64 value |
0x03 |
MIME type (UTF-8) | 2-byte length prefix + bytes |
0x04 |
File content | 4-byte length prefix + raw bytes |
The encode() method serializes these fields into a single binary blob, while decode() performs the reverse operation. Critical validation occurs at lines 29-66: the implementation enforces FileTransferLimits.maxPayloadBytes (approximately 10 MB) and guarantees non-empty content via guard !content.isEmpty.
This structure serves dual purposes—it represents both in-memory working data and on-wire transmission format, eliminating translation layers that could introduce vulnerabilities.
Encryption Architecture: Noise Protocol Integration
When transferring private media between peers with established sessions, BitChat leverages the Noise cryptographic framework. The entry point is [NoiseEncryptionService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NoiseEncryptionService.swift#L91-L104), specifically the encryptPrivateFilePayload method.
Encryption Requirements
- Payload type byte: Must be
NoisePayloadType.privateFile(0x20) - Size validation:
NoiseSecurityValidator.validatePrivateFileMessageSizechecks against protocol limits - Rate limiting: Enforced before encryption proceeds
- Session state: If no Noise session exists, the call triggers
onHandshakeRequiredand queues the operation
The encryption produces a Noise payload that encapsulates the complete BitchatFilePacket TLV data, ensuring confidentiality even when broadcast across the BLE mesh to intermediate nodes.
BLE Transport and Policy Enforcement
Actual transmission occurs through [BLEService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift#L40-L78), which exposes two distinct APIs:
sendFileBroadcast(_:transferId:)— Public file transfers without encryptionsendFilePrivate(_:to:transferId:allowLegacyFallback:)— Private media with full encryption pipeline
Private Send Flow (Lines 40-78)
The sendFilePrivate implementation executes four sequential stages:
-
Admission control — Registers transfer with
privateMediaTransferAdmissionsto prevent resource exhaustion -
Policy evaluation — Queries
privateMediaSendPolicyfor the target peer, yielding one of three outcomes:encrypted: Proceed with Noise encryptionlegacyRequiresConsent: Require explicit user consent, then fallback to signed-legacy pathblockedDowngrade: Reject transmission entirely
-
Payload construction —
BLENoisePayloadFactory.privateFile(filePacket)prepares the encrypted container -
Session management — Existing sessions trigger immediate
makeEncryptedNoisePacket+broadcastPacket; missing sessions queue viapendingNoiseSessionQueuesand initiate handshake
Legacy Fallback Path
When allowLegacyFallback: true is specified and the peer lacks encrypted private media capability, the system invokes sendSignedLegacyPrivateFile (lines 89-112). This constructs a standard BitchatPacket containing raw TLV data, signs it through the Noise service, and broadcasts without payload encryption. This path requires explicit caller opt-in due to reduced security guarantees.
Migration-Aware Payload Handling
BitChat's protocol evolution necessitates backward compatibility during the transition from signed raw files to end-to-end encrypted media. The system distinguishes four inbound payload categories per [PRIVATE-MEDIA-MIGRATION.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/PRIVATE-MEDIA-MIGRATION.md):
| Sender Version | Wire Indicator | Processing |
|---|---|---|
| Current Android | Noise 0x20 |
Decrypt and deliver |
| Prerelease iOS | Noise 0x09 (alias) |
Decrypt, canonicalize to 0x20, deliver |
| Legacy clients | Signed directed fileTransfer |
Signature verification, then deliver |
| Unsigned/forged | Raw directed fileTransfer |
Reject |
This discrimination prevents downgrade attacks while maintaining service during protocol upgrades across the fragmented device ecosystem.
Receipt Verification and Message Identity
BitChat implements cryptographic receipts without extending the TLV format. The PrivateMediaMessageIdentity utility (lines 158-230 in BitchatFilePacket.swift) derives verification keys from the packet's intrinsic fields—filename, size, and MIME type—enabling Android receivers to validate signatures using only standard packet contents.
Stable identifiers are computed deterministically from:
- Sender ID
- Recipient ID
- Filename
This construction ensures receipt matching survives device synchronization and multi-device scenarios.
Practical Implementation
// Construct a voice note packet from raw audio data
let voiceData = try Data(contentsOf: recordedAudioURL)
let packet = BitchatFilePacket(
fileName: "voice_\(UUID().uuidString.prefix(8)).m4a",
fileSize: UInt64(voiceData.count),
mimeType: "audio/m4a",
content: voiceData
)
// Transmit with mandatory encryption (no legacy fallback)
bleService.sendFilePrivate(
packet,
to: destinationPeerID,
transferId: UUID().uuidString,
allowLegacyFallback: false
)
For controlled interoperability with older clients:
// User-consented legacy transmission for backward compatibility
bleService.sendFilePrivate(
packet,
to: destinationPeerID,
transferId: UUID().uuidString,
allowLegacyFallback: true // Requires: policy == .legacyRequiresConsent
)
These calls chain through the exact implementation paths documented in BLEService.swift, with encoding handled by BitchatFilePacket.encode().
Key Source References
| Component | Location |
|---|---|
| TLV packet definition | [Bitchat/Protocols/BitchatFilePacket.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/BitchatFilePacket.swift) |
| Encoding/decoding logic | Lines 29-66 |
| Identity utilities | Lines 158-230 |
| Noise encryption service | Bitchat/Services/NoiseEncryptionService.swift#L91-L104 |
| BLE transport & policy | Bitchat/Services/BLE/BLEService.swift#L40-L78 |
| Legacy fallback implementation | Lines 89-112 |
| Migration specification | [docs/PRIVATE-MEDIA-MIGRATION.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/PRIVATE-MEDIA-MIGRATION.md) |
Summary
- Private media storage uses
BitchatFilePacket, a TLV-encoded structure with four typed fields (filename, size, MIME, content) and strict 10 MB limits. - Encryption applies Noise protocol with payload type
0x20, enforced byNoiseEncryptionService.swiftwith size validation and rate limiting. - BLE transport routes through
sendFilePrivate, which implements admission control, policy evaluation, and automatic session negotiation. - Compatibility is maintained via migration-aware payload classification and opt-in legacy fallback with explicit user consent.
- Receipt security derives verification keys from packet contents without protocol extensions, using deterministic stable IDs for cross-device matching.
Frequently Asked Questions
What is the maximum file size for private media in BitChat?
BitChat enforces a ~10 MB payload limit through FileTransferLimits.maxPayloadBytes. This ceiling applies to the complete TLV-encoded BitchatFilePacket after field serialization, not raw file data alone. The implementation rejects oversized transfers at encoding time with guard assertions in BitchatFilePacket.swift.
Can BitChat send files to older app versions that don't support encryption?
Yes, through controlled downgrade with allowLegacyFallback: true. This triggers the sendSignedLegacyPrivateFile path, which transmits signed but unencrypted payloads. However, the operation requires: (1) explicit caller opt-in, (2) peer policy of legacyRequiresConsent, and (3) user confirmation—preventing silent security degradation.
How does BitChat prevent forged private media injections?
Inbound processing根据 [PRIVATE-MEDIA-MIGRATION.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/PRIVATE-MEDIA-MIGRATION.md) 分类 payload 来源。Unsigned raw fileTransfer packets are rejected entirely. Signed legacy payloads undergo signature verification. Modern encrypted payloads require valid Noise session state and payload type 0x20. This layered validation closes injection vectors across protocol versions.
Why does BitChat use TLV instead of JSON or Protobuf for file packets?
TLV provides determinism and compactness critical for BLE mesh constraints. The binary format guarantees identical wire representations across iOS and Android, eliminates parsing ambiguities, and supports streaming validation. The fixed tag scheme (0x01–0x04) enables forward-compatible parsing without schema registries—essential for decentralized peer-to-peer networks without central coordination.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →