Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/permissionlesstech/bitchat/llms.txt

Use this file to discover all available pages before exploring further.

Bitchat uses layered encryption across its two transports. Over the Bluetooth mesh, live peer sessions are protected by the Noise Protocol Framework; courier envelopes destined for offline recipients are sealed with a one-way Noise variant. Over the Nostr internet path, private messages travel inside a proprietary double-sealed envelope encrypted with XChaCha20-Poly1305. Every private payload — messages, delivery acknowledgements, read receipts — rides inside one of these schemes; intermediate relay nodes forward opaque ciphertext and never see plaintext.

Live Sessions: Noise XX

When two devices are in BLE range and at least one initiates a direct connection, they perform a three-message Noise XX handshake before any application data flows.
XX:
  -> e
  <- e, ee, s, es
  -> s, se
Each arrow is a BLE packet. After the third message both sides hold a pair of symmetric cipher states derived from the combined handshake transcript and begin encrypting application data immediately. What the XX pattern provides:
  • Mutual authentication — both parties prove possession of their static Curve25519 private key by the end of the handshake.
  • Forward secrecy — fresh ephemeral keys are generated for every handshake. Compromise of a device’s long-term static key does not expose past sessions.
  • Identity hiding — static public keys are encrypted inside the handshake. A passive observer on the radio sees only ephemeral key material until after the first DH exchange.
  • Replay protection — the implementation uses a 1024-message sliding window to reject duplicate nonces.
  • Automatic rekey — sessions rekey after one hour or 10,000 messages, whichever comes first.
Scope: all private messages, delivery acks, and read receipts that flow over an active BLE connection ride inside a Noise XX transport session.

Swift Class Signatures

The core classes from the implementation:
// NoiseEncryptionService — main service managing all Noise operations
final class NoiseEncryptionService {
    private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
    private let sessionManager: NoiseSessionManager
    private let channelEncryption = NoiseChannelEncryption()
}

// NoiseSession — per-peer session state
final class NoiseSession {
    private var handshakeState: NoiseHandshakeState?
    private var sendCipher: NoiseCipherState?
    private var receiveCipher: NoiseCipherState?
    private let remoteStaticKey: Curve25519.KeyAgreement.PublicKey?
}

// NoiseSessionManager — thread-safe session table
final class NoiseSessionManager {
    private var sessions: [String: NoiseSession] = [:]
    private let sessionsQueue = DispatchQueue(label: "noise.sessions", attributes: .concurrent)
}
Encrypt and decrypt operations once a session is established:
// Encrypt message for a peer
let encrypted = try noiseService.encrypt(messageData, for: peerID)

// Decrypt a received message
let decrypted = try noiseService.decrypt(encryptedData, from: peerID)

Error Handling

enum NoiseError: Error {
    case uninitializedCipher
    case invalidCiphertext
    case handshakeComplete
    case handshakeNotComplete
    case missingLocalStaticKey
    case missingKeys
    case invalidMessage
    case authenticationFailure
    case invalidPublicKey
    case replayDetected
    case nonceExceeded
}

Offline Seals: Noise X

When neither a live BLE link nor a Nostr path can deliver a message promptly, the sender seals it into a courier envelope using the one-way Noise X pattern:
X:
  -> e, es, s, ss
The entire payload travels in a single message. The sender’s static key is authenticated inside the ciphertext — the recipient can verify who sent it — but the sender has no way to confirm delivery or perform a DH with a recipient-supplied ephemeral. Courier envelopes are handed to up to three nearby peers who physically carry them until they encounter the recipient. Couriers hold only opaque ciphertext and a 16-byte rotating recipient tag; they cannot read the content or identify the sender.
No forward secrecy. Noise X seals to the recipient’s static Curve25519 key. If that key is ever compromised — including by a device seizure — an attacker who holds sealed-but-undelivered envelopes can decrypt them. A prekey-based forward-secrecy scheme for courier envelopes is listed as future work in the protocol design.

Nostr Private Messages

When two mutual favorites are not in BLE range but both have internet access, Bitchat routes private messages over Nostr relays. The format is Bitchat’s own proprietary double-sealed envelope — it reuses NIP-17 and NIP-59 kind numbers purely for relay-compatibility reasons, but the cryptography is entirely different.

Envelope Structure

Kind 14 — Inner Message

The actual message content. Unsigned; never published directly to any relay.

Kind 13 — Seal

The kind 14 content, encrypted and placed inside a sender-signed event. Sender identity is inside the ciphertext.

Kind 1059 — Gift Wrap

The kind 13 seal, encrypted again inside an outer envelope signed by a randomly-generated one-time key. This is what relays actually store.
Relays see: the recipient’s Nostr public key (outer p tag), event timing and size, and network metadata. They do not see message content or the sender’s stable identity.

Encryption: XChaCha20-Poly1305

Each encrypted content field is formatted as:
v2:<base64url(24-byte nonce || ciphertext || 16-byte Poly1305 tag)>
The v2: prefix identifies the encoding version. Encryption uses XChaCha20-Poly1305 with a 24-byte random nonce. The implementation derives a ChaCha20 subkey via HChaCha20 on the first 16 bytes of the nonce, then encrypts with the remaining 8 bytes padded to a 12-byte ChaCha20 nonce.

Key Derivation

Keys come from secp256k1 ECDH between the sender’s and recipient’s Nostr key pairs, fed through HKDF-SHA256. The derivation reuses the "nip44-v2" info label for historical reasons but follows a different key schedule from NIP-44 and is not interoperable with it.

Timestamp Randomization

Outer seal and envelope event timestamps are randomized by up to ±15 minutes. The real message timestamp is encrypted inside the kind 14 inner payload and never visible to relays.
Compatibility: Bitchat’s Nostr envelope format reuses kind numbers 14, 13, and 1059 but is not compatible with NIP-17, NIP-44, or NIP-59. It only interoperates with other Bitchat clients. Standard Nostr clients cannot decrypt these messages and should not try.No forward secrecy. Compromise of the recipient’s static Nostr private key can expose all stored envelopes addressed to that key on any relay that has retained them.

Padding

Payload length is a known metadata leak. Bitchat addresses this selectively: Padded packet types (noiseEncrypted, noiseHandshake): Noise-encrypted packets are padded toward bucket boundaries of 256, 512, 1024, or 2048 bytes before encryption. Padding is PKCS#7 style — each pad byte contains the pad length. Because the pad length must fit in one byte, a frame that would need more than 255 bytes of padding to reach its bucket is emitted unpadded. Unpadded packet types (everything else): Public channel messages, signed announces, gossip-sync packets, fragmented transfers, group messages, and board posts all go out at their natural length.
Payload length is observable for all non-Noise packet types. An observer in radio range can infer message sizes for public traffic. Padding for non-Noise packet types, and closing the edge case where very large frames are emitted unpadded, are listed as future work.

Cryptographic Primitives Summary

AlgorithmUseDetails
Curve25519 (X25519)Noise DH operationsAll Noise XX and Noise X handshakes
ChaCha20-Poly1305Noise AEAD cipherSession encryption, handshake key material, outbox sealing
SHA-256Noise hash, peer ID derivationHandshake transcript, chaining key, peer ID fingerprint
HKDF-SHA256Noise KDF, Nostr key derivationSession key split, Nostr ECDH key derivation
HChaCha20XChaCha20 subkey derivationNostr envelope encryption
XChaCha20-Poly1305Nostr envelope AEADKind 13 and kind 1059 content fields
secp256k1 ECDHNostr key agreementShared secret for Nostr envelope keys
Ed25519Packet signaturesSigned announces, packet authentication
AES-GCMLocal identity stateSome protected local identity storage

Build docs developers (and LLMs) love