BitChat

repository·main·Indexed 12 days ago

https://github.com/permissionlesstech/bitchat

A decentralized, peer-to-peer messaging application supporting offline communication via Bluetooth mesh and online communication via the Nostr protocol. It features a dual transport architecture, end-to-end encryption using the Noise Protocol Framework (XX pattern), and location-based channels using geohash precision. The project is written in Swift and includes a modular ChatViewModel architecture and a MockBLEService test harness for deterministic networking simulation.

Tokens
23.8K
Snippets
37
Records
113
Agent score
98%

What's inside BitChat

  1. Understand the PTT implementation roadmap

    main

    The Push-to-Talk feature is being rolled out in three distinct phases:

    1. Phase 1 — DM live: Focuses on high-value, low-blast-radius Direct Messages. Includes the encoder, framer, Noise inner type, assembler/player, live bubble UI, and 'finalize-as-note' deduplication. Single-hop DMs are the primary use case here.
    2. Phase 2 — Public Mesh: Introduces voiceFrame 0x29, signing/verification, relay policies, floor-courtesy UI, and autoplay defaults.
    3. Phase 3 (v2 candidates): Future enhancements including background audio mode, mid-burst repair requests (via REQUEST_SYNC), talk-over mixing, AAC-ELD low-delay profiles, a dedicated walkie-mode screen, and media-over-Nostr for internet peers.
  2. Understand the BLE Transport Architecture V3

    main

    The BLE Transport Architecture V3 is designed to separate physical link management from protocol logic using a 'sans-I/O' engine model.

    Key architectural shifts include:

    • Ownership of Bindings: Link authentication and peer-to-link bindings (BLELinkAuthState and BLELinkBindings) are owned by the engine rather than the Bluetooth queue (bleQueue). This ensures atomicity: security checks and actions (like sending) are serialized against rebinds to prevent Time-of-Check to Time-of-Use (TOCTOU) vulnerabilities.
    • Separation of Concerns: The link layer (bleQueue) is responsible only for physical state (CoreBluetooth objects, connection lifecycles, and backpressure buffers) keyed by opaque linkIDs. The engine is responsible for resolving sender bindings and handling protocol logic.
    • Event-Driven Communication: The link layer communicates with the engine via BLELinkEvent (e.g., frameDecoded and physical lifecycle transitions) using the emitLinkEvent method, which the engine consumes via handleLinkEvent.
  3. Understand the ConversationStore architecture

    main

    The ConversationStore serves as the single source of truth for all message state in bitchat. It is an @MainActor owned object (via AppRuntime) that manages message data through a collection of Conversation objects.

    Key Concepts

    • Conversation as a Reference Type: Each Conversation (identified by ConversationID such as .mesh, .geohash, or .direct) is an ObservableObject. This allows UI components and feature models to observe a specific conversation directly. An update to one conversation does not invalidate observers of other conversations.
    • Incremental Updates: Unlike legacy implementations that performed full-dictionary replacements, the ConversationStore uses an incremental approach. Appending a message updates only that specific conversation's message index.
    • Intent-Based Mutations: To ensure data integrity, the store does not allow direct mutation of its backing collections. Instead, consumers must use a dedicated intent API to request changes.
    • Synchronous Reads: Because the store and its readers share the @MainActor, reads are synchronous. Once an append operation returns, all observers see the updated state immediately, eliminating the need for manual synchronization bridges.
  4. Understand bitchat's dual transport architecture

    main

    bitchat uses a hybrid messaging architecture to provide both offline and online communication:

    Bluetooth Mesh Network (Offline)

    • Scope: Local peer-to-peer communication within Bluetooth range.
    • Routing: Supports multi-hop relay (up to 7 hops) through nearby devices.
    • Security: Uses the Noise Protocol for end-to-end encryption with forward secrecy for live sessions.
    • Requirement: No internet required.

    Nostr Protocol (Internet)

    • Scope: Global reach via internet-based Nostr relays.
    • Channels: Uses geohash coordinates for location-based chat rooms.
    • Security: Uses BitChat Private Envelopes (proprietary XChaCha20-Poly1305 construction) inside kind-1059 events. Note: This is NOT NIP-17, NIP-44, or NIP-59 compatible.
    • Requirement: Internet access required.
  5. Secure Source Routed Packets with Ed25519

    main

    Source routing is secured by the existing Ed25519 signature scheme. The signature covers the entire packet structure (Header + Sender + Recipient + Route + Payload).

    Integrity Guarantee: Any tampering with the route list by a relay will invalidate the signature, causing the destination to drop the packet.

    Signature Input Construction: To verify or sign, serialize the packet exactly as transmitted, but:

    1. Set TTL = 0 temporarily.
    2. Remove the Signature bytes.
  6. Understand the Smart Push-to-Talk (PTT) delivery strategy

    main

    Smart PTT is a delivery strategy that automatically selects the best transport method for voice based on the conversation context. It transitions from live streaming to reliable voice notes to ensure the best user experience without requiring manual mode switching.

    ContextDelivery Strategy
    DM (Peer reachable on mesh)Live stream (Noise-encrypted frames) + finalized voice note for reliability
    DM (Peer only via Nostr)Existing voice-note recording only (no live streaming)
    Public mesh chatLive broadcast stream (signed) + finalized voice note
    Geohash (Nostr) channelsPTT unavailable (follows existing media policy)

    This approach allows the system to degrade gracefully from live $\rightarrow$ reliable-note $\rightarrow$ unavailable based on transport capabilities.

  7. Understand the AppRuntime and AppEventStream architecture

    main

    The architecture has moved toward a composition root pattern centered around AppRuntime. Instead of the app shell (BitchatApp) or a single global ChatViewModel managing all logic, AppRuntime now handles:

    • Startup and Lifecycle: Orchestrates the initial boot and app lifecycle.
    • Notification Routing: Manages how system and app-level notifications are distributed.
    • Shared Content Intake: Acts as the entry point for shared data.
    • Service Ownership: Manages long-running observers like Tor, screenshot monitoring, and Nostr reconnection logic.

    To interact with app-level events, use AppEventStream, which provides a typed, asynchronous event surface. This replaces older patterns of using delegates or broad notification fan-outs, allowing for more predictable and type-safe event handling.

  8. Handle Noise session rehandshakes after decryption failure

    main

    Bitchat has moved away from legacy NACK recovery. Instead, session recovery relies on a Noise session rehandshake following a decryption failure or desynchronization.

    Recovery Pattern

    When a decryption error occurs, you must proactively clear the local session and re-initiate a handshake. The peer receiving the new handshake will accept it and replace their existing session.

    Implementation Steps

    1. Detect decryption failure.
    2. Call removeSession(for:) on the NoiseSessionManager for the affected peer (this prevents alreadyEstablished errors).
    3. Call initiateHandshake(with:) to start the new session.
    // Example recovery flow in a test context
    // 1. Induce error (e.g., by corrupting ciphertext)
    // 2. Clear existing session to allow re-initiation
    noiseManager.removeSession(for: targetPeerID)
    
    // 3. Re-initiate handshake
    noiseManager.initiateHandshake(with: targetPeerID)
    
    // 4. Verify subsequent encrypt/decrypt operations succeed
  9. Recognize mutual favorites using pairwise recognition tags

    main

    Since static keys are not broadcast in v2 announces, devices use pairwise recognition tags to identify mutual favorites. A tag is a directional 8-byte MAC that only the two participating peers can compute.

    Tag Generation (A $\to$ B):

    1. Compute shared secret S_AB = X25519(A_noiseStaticPrivate, B_noiseStaticPublic).
    2. Derive K_AB = HKDF-SHA256(ikm: S_AB, salt: "", info: "bitchat-recognition-v1", length: 32).
    3. Compute tag_A\to B = HMAC-SHA256(key: K_AB, message: uint32be(epoch) || A_noiseStaticPublic || B_noiseStaticPublic || peerID_e)[0..8].

    Implementation Rules:

    • Directional: The tag must include the sender's and receiver's public keys in a specific order so that tag_A\to B \neq tag_B\to A. This prevents observers from linking two rotating IDs via a symmetric tag.
    • Binding: The peerID_e must be included in the HMAC message to prevent attackers from replaying a tag from one ID to another.
    • Padding: The tag list in an announce must be padded with uniform random 8-byte values to a fixed count of TAG_SLOTS = 8. This hides the actual number of mutual favorites.
    • Rotation: If a device has more than 8 favorites, it must rotate which favorites occupy the slots across successive announces.
    • Unidirectional Caution: Do not include a tag for a one-directional favorite; this would disclose interest to a peer who hasn't reciprocated.

    Warning: Recognition is a hint only. Do not perform consequential actions (like routing DMs or showing verified badges) based on a tag match alone. Wait for a completed Noise handshake.

    S_AB    = X25519(A_noiseStaticPrivate, B_noiseStaticPublic)
    K_AB    = HKDF-SHA256(ikm: S_AB, salt: "", info: "bitchat-recognition-v1", length: 32)
    
    tag_A\to B = HMAC-SHA256(key: K_AB,
                          message: uint32be(epoch)
                                || A_noiseStaticPublic   (32)
                                || B_noiseStaticPublic   (32)
                                || peerID_e              (8))[0..8]
  10. Configure PTT Playback Policy

    main

    To prevent unwanted audio, PTT follows strict playback rules. Developers/Users should be aware of the following logic:

    1. Autoplay Requirements: Live audio only autoplays if:
      • The app is in the foreground.
      • The burst's conversation is currently on screen.
      • The "live voice messages" toggle is enabled in the app-info sheet.
    2. Live Bubbles: If autoplay conditions are not met, the burst appears as a live bubble (pulsing waveform + LIVE badge). Tapping the bubble joins playback at the live edge.
    3. Concurrency: Only one voice can play at a time via the VoiceNotePlaybackCoordinator. If a second person speaks while a burst is active, the second burst is shown as a tappable live bubble instead of mixing audio.
    4. Notifications: For non-focused DMs, a single notification is fired at the START of the burst, not for every frame.
  11. Understand the Arti binary provenance and source inputs

    main

    The arti binary target is provided as a prebuilt static-library xcframework located at localPackages/Arti/Frameworks/arti.xcframework. This is linked via SwiftPM through localPackages/Arti/Package.swift.

    When updating or auditing this dependency, the following source inputs define the binary's provenance:

    • Rust workspace: localPackages/Arti/Cargo.toml
    • Crate: localPackages/Arti/arti-bitchat
    • Dependency lockfile: localPackages/Arti/Cargo.lock (Requires Rust 1.90+)
    • Build script: localPackages/Arti/build-ios.sh (Targets aarch64-apple-ios, aarch64-apple-ios-sim, x86_64-apple-ios, aarch64-apple-darwin, and x86_64-apple-darwin)
    • Exported C header: localPackages/Arti/Frameworks/include/arti.h

    The build process uses size-oriented flags (opt-level=z, fat LTO, one codegen unit, panic=abort, stripped symbols) and normalizes static-archive metadata using xcrun libtool -static -D to ensure stable hashes.

  12. Understand the Private-media Wire Migration

    main

    Bitchat is migrating how private files are transmitted over the wire to ensure end-to-end encryption (E2EE) via Noise sessions.

    Wire Formats

    • Preferred (E2EE): The BitchatFilePacket TLV is encrypted inside the peer's Noise session before BLE fragmentation. This uses NoisePayloadType.privateFile (0x20).
    • Legacy (Prerelease iOS): Some iOS builds use 0x09. Decoders canonicalize this to privateFile but do not emit it.
    • Legacy (Older Clients): Uses a signed, directed fileTransfer. This is not confidential; mesh relays can see the raw file TLV. The UI requires explicit per-send user consent for this path.

    Payload Types

    • NoisePayloadType.privateFile (0x20): The standard for encrypted private media.
    • NoisePayloadType.authenticatedPeerState (0x21): A permanent protocol type used to exchange peer capabilities and bind identities. It is emitted after every completed/rekeyed Noise XX session.

    Security and Pinning

    • Pinning: A peer is only considered "pinned" (authenticated) once a successfully decrypted 0x21 state is received. This binds the Noise fingerprint to the Ed25519 key.
    • Fallback Security: The legacy fileTransfer fallback is signed. Relays cannot forge the sender or contents, but they can see the data. This fallback is only available to unpinned peers with a stable Noise key.