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.validatePrivateFileMessageSize checks against protocol limits
  • Rate limiting: Enforced before encryption proceeds
  • Session state: If no Noise session exists, the call triggers onHandshakeRequired and 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 encryption
  • sendFilePrivate(_:to:transferId:allowLegacyFallback:) — Private media with full encryption pipeline

Private Send Flow (Lines 40-78)

The sendFilePrivate implementation executes four sequential stages:

  1. Admission control — Registers transfer with privateMediaTransferAdmissions to prevent resource exhaustion

  2. Policy evaluation — Queries privateMediaSendPolicy for the target peer, yielding one of three outcomes:

    • encrypted: Proceed with Noise encryption
    • legacyRequiresConsent: Require explicit user consent, then fallback to signed-legacy path
    • blockedDowngrade: Reject transmission entirely
  3. Payload construction — BLENoisePayloadFactory.privateFile(filePacket) prepares the encrypted container

  4. Session management — Existing sessions trigger immediate makeEncryptedNoisePacket + broadcastPacket; missing sessions queue via pendingNoiseSessionQueues and 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 by NoiseEncryptionService.swift with 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →