How Private Media Is Stored and Encrypted in BitChat: A Noise Protocol Deep Dive

BitChat encrypts private media using Noise XX handshakes with X25519 key agreement and ChaCha20-Poly1305 authenticated encryption, storing only ciphertext in the Application Support directory with automatic 100 MB quotas and seven-day expiration policies.

BitChat, the open-source mesh messaging application from the permissionlesstech/bitchat repository, treats images, voice notes, and files as end-to-end encrypted payloads that remain opaque to intermediate relays. Understanding how private media is stored and encrypted in BitChat requires examining the interplay between the Noise cryptographic protocol, BLE capability negotiation, and iOS sandboxed storage policies.

End-to-End Encryption Architecture

BitChat wraps all private-media packets in a Noise XX handshake envelope. The implementation utilizes X25519 for elliptic-curve key agreement, ChaCha20-Poly1305 for authenticated encryption, and SHA-256 for hashing, as documented in the project's privacy assessment.

When a peer advertises the private-media capability (bit 8), the application transmits media as an XChaCha20-Poly1305-encrypted noiseEncrypted payload. This ciphertext is completely opaque to mesh nodes and BLE relays; only the intended recipient possessing the correct session keys can decrypt the content after completing the Noise handshake.

Capability Verification and Session Management

Before transmitting encrypted content, BitChat verifies that the remote peer actually supports private media reception. The BLEPrivateMediaSessionStore class, located in bitchat/Services/BLE/BLEPrivateMediaSessionStore.swift, manages per-peer state including current Noise generations, advertised capabilities, and pending policy-resolution waiters.

The store enforces strict timeout watchdogs for capability-proof exchanges, ensuring that stale or forged proofs cannot authorize media transmission. According to lines 15–20 of BLEPrivateMediaSessionStore.swift, these timeouts prevent ambiguous session states from lingering beyond policy limits.

Receipt-Aware Capability Checking

The store distinguishes between basic private media (bit 8) and durable receipts (bit 9). When an application requests receipt generation, the method provenGeneration(for:requireReceipts:) validates that the peer proved both capabilities before authorizing transmission. As implemented in lines 68–77 of BLEPrivateMediaSessionStore.swift, this guarantees that receipt-guaranteed media only flows to nodes capable of storing and retrying failed deliveries.

On-Device Storage Format and Policies

Private media files never exist in cleartext on disk. BitChat writes incoming files to the Application Support directory as raw Noise-encrypted ciphertext. The privacy assessment specifies a 100 MB oldest-first quota for incoming media and automatic clearance of all managed media after seven days.

A panic-wipe operation exposed through BLEPrivateMediaSessionStore.panicReset() immediately destroys session state and removes outstanding media files. Lines 33–36 of the session store implementation handle this atomic cleanup.

Storage Quotas and Automatic Expiration

The storage subsystem enforces size-based eviction when the 100 MB threshold is exceeded, prioritizing the oldest files for deletion. Combined with the mandatory seven-day retention limit, these policies ensure that encrypted media does not accumulate indefinitely on user devices.

The Private Media Delivery Workflow

The transmission process follows a strict four-phase pipeline that ensures encryption occurs before any network exposure.

1. Capability Validation

The sender queries BLEPrivateMediaSessionStore.provenGeneration(for:requireReceipts:) to determine if the peer advertises the private-media capability bit. If the check fails, BitChat falls back to sending the file in a signed cleartext envelope that mesh relays can inspect, as shown in lines 68–78 of the session store.

2. Noise Encryption

Valid sessions trigger encryption using the established Noise session keys, producing an opaque noiseEncrypted blob that encapsulates the media content.

3. BLE Transmission

The encrypted blob passes to BLEFileTransferHandler, which queues the data for delivery over the mesh network while respecting the private-media capability flag. Lines 50–58 of BLEFileTransferHandler.swift manage this queuing logic and watchdog logging.

4. Persistent Ciphertext Storage

Upon receipt, the ciphertext writes directly to the media folder under Application Support without intermediate decryption. The file remains encrypted until the receiving peer completes the Noise handshake and explicitly decrypts the content for display.

Code Examples

// Check if a peer can receive encrypted private media with receipts
if let generation = BLEPrivateMediaSessionStore.shared.provenGeneration(
        for: peerID,
        requireReceipts: true) {
    // Peer supports encrypted private media + receipts → send encrypted blob
    sendEncryptedMedia(to: peerID, using: generation)
} else {
    // Fallback: send a signed, clear-text envelope (mesh relays will see content)
    sendSignedEnvelope(to: peerID, mediaData)
}
// Store an inbound private-media file (ciphertext only)
func storeIncomingMedia(_ ciphertext: Data, for messageID: String) {
    let mediaURL = MediaDirectory.privateMedia.appendingPathComponent(messageID)
    // Ciphertext stays encrypted on disk
    try? ciphertext.write(to: mediaURL, options: .atomic)
}
// Panic-wipe removes all private-media state and files
func wipeAllPrivateMediaOnPanic() {
    BLEPrivateMediaSessionStore.shared.panicReset()   // Clears session & receipt data
    try? FileManager.default.removeItem(at: MediaDirectory.privateMedia) // Delete files
}

Summary

  • Noise XX Protocol: All private media uses X25519 key agreement with ChaCha20-Poly1305 authenticated encryption and SHA-256 hashing.
  • Capability Verification: BLEPrivateMediaSessionStore validates peer capabilities (bit 8) and optional receipt support (bit 9) before transmission.
  • Ciphertext-Only Storage: Files persist only in Application Support as encrypted blobs, subject to 100 MB quotas and seven-day automatic expiration.
  • Four-Phase Workflow: Capability check → Noise encryption → BLE transmission → persistent ciphertext storage.
  • Panic Wipe: panicReset() provides immediate destruction of session keys and media files.

Frequently Asked Questions

What encryption algorithm does BitChat use for private media?

BitChat implements the Noise XX handshake pattern using X25519 for elliptic-curve Diffie-Hellman key exchange, ChaCha20-Poly1305 for authenticated encryption, and SHA-256 for integrity hashing. This combination provides forward secrecy and ensures that only the intended recipient can decrypt media files.

Where does BitChat store private media files on iOS?

BitChat stores private media exclusively in the Application Support directory as Noise-encrypted ciphertext. The application never writes cleartext media to disk, and enforces a 100 MB oldest-first quota with automatic deletion of files older than seven days according to the privacy assessment documentation.

How does BitChat prevent unauthorized peers from receiving encrypted media?

The BLEPrivateMediaSessionStore validates capability proofs through the provenGeneration method before transmission. If a peer does not advertise the private-media capability bit, BitChat sends the file in a signed cleartext envelope instead, preventing wasteful encryption to incompatible nodes and ensuring transparency when end-to-end encryption is unavailable.

What happens to private media during a panic wipe?

Invoking BLEPrivateMediaSessionStore.panicReset() immediately clears all session generations, capability proofs, and receipt data from memory and persistent stores. Simultaneously, the application deletes the private media directory from Application Support, ensuring no ciphertext or session metadata remains on the device.

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 →