# How Private Media Is Stored and Transferred in BitChat: A Deep Dive into BLE Mesh File Protocols

> Discover how BitChat stores and transfers private media using BLE Mesh File Protocols. Learn about TLV-encoded packets, Noise protocol encryption, and robust transport mechanisms.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-05

---

**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](https://github.com/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)](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/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/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/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`](https://github.com/permissionlesstech/bitchat/blob/main/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

```swift
// 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:

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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)](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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NoiseEncryptionService.swift#L91-L104) |
| BLE transport & policy | [`Bitchat/Services/BLE/BLEService.swift#L40-L78`](https://github.com/permissionlesstech/bitchat/blob/main/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)](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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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/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.