BitChat Courier Store-and-Forward Service for Offline Messaging: Technical Deep Dive
The BitChat courier store-and-forward service guarantees message delivery to offline peers by having mutual favorites physically transport end-to-end encrypted envelopes through a spray-and-wait diffusion protocol, using rotating recipient tags and bounded copy-budgets to prevent metadata leakage and amplification attacks.
BitChat, developed by the open-source team at permissionlesstech, solves the challenge of messaging when recipients are unreachable via live Bluetooth Low Energy (BLE) mesh or Nostr relays. The BitChat courier store-and-forward service for offline messaging enables mobile devices to carry opaque encrypted payloads for one another, ensuring eventual delivery without exposing message contents to intermediate devices or enabling long-term traffic analysis.
The Courier Envelope Architecture
At the core of the offline messaging layer lies the courier envelope, a Type-Length-Value (TLV) structure defined in localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift. This opaque blob wraps private messages in a format that couriers can transport without gaining access to the underlying plaintext.
The envelope contains:
- A rotating recipient tag derived from the recipient’s Noise static public key
- An expiry timestamp enforcing bounded lifetime
- End-to-end encrypted ciphertext
- A copy-budget field limiting diffusion hops
- An optional pre-key ID for forward secrecy
Because the envelope appears as random bytes to anyone except the intended recipient, couriers act as oblivious data mules, unable to read, modify, or meaningfully analyze the traffic they carry.
Rotating Recipient Tags for Privacy
To prevent long-term correlation attacks while still enabling routing, BitChat implements rotating recipient tags that change daily and cannot be linked across time periods. The tag is derived from the recipient’s Noise static public key and the current UTC day, calculated via CourierEnvelope.recipientTag(noiseStaticKey:epochDay:) in CourierEnvelope.swift.
When a device receives an envelope, it checks candidate tags using CourierEnvelope.candidateTags(noiseStaticKey:around:), generating possible tags for the current day and surrounding days to account for clock drift. This design ensures that couriers see only a temporary routing hint that expires with the day, eliminating persistent metadata trails that could identify communication patterns.
Spray-and-Wait Diffusion with Copy-Budgets
The service employs a spray-and-wait diffusion strategy to propagate envelopes through moving crowds while preventing amplification attacks. Each envelope carries a copies field with a maximum budget of 8. When a courier accepts an envelope for forwarding, the copy budget is halved at each "spray" event, limiting how many times the message can split and preventing an attacker from flooding the network by cloning envelopes.
This bounded epidemic routing ensures that messages spread through a mobile device population efficiently without consuming unbounded bandwidth or storage. The outbox retention logic—described in the WHITEPAPER.md (section 5.2)—retains envelopes until at least three distinct couriers have accepted them, or until the envelope expires, whichever comes first.
Deposit, Retry, and Expiration Logic
Reliability is enforced through automatic deposit and retry mechanisms. The sender’s outbox, managed by the MessageRouter, retains envelopes and automatically re-deposits them when new eligible couriers appear within radio range. This ensures that transient connectivity gaps do not result in message loss.
Expiration is enforced by the maxLifetimeSeconds constant (defaulting to 24 hours) and the isExpired(at:) method in CourierEnvelope.swift. Stale envelopes are discarded by the CourierStore, preventing storage exhaustion and replay attacks. The combination of expiry and copy-budget limits creates a finite window during which the message must reach its destination, after which it is safely purged from all participating devices.
Bridge Couriers and Nostr Fallback
For scenarios where no direct mesh path exists between sender and recipient, BitChat supports bridge courier drops that allow devices acting as bridges to forward opaque envelopes over Nostr relays. The BridgeCourierService (tested in bitchatTests/Services/BridgeCourierServiceTests.swift) handles this flow, ensuring the bridge device cannot read the payload while extending the store-and-forward capability beyond local BLE range to the global Nostr network.
This hybrid approach combines the low-latency efficiency of local mesh diffusion with the reach of internet relay networks, all while maintaining strict end-to-end encryption boundaries.
Implementing Store-and-Forward in Code
The following Swift examples demonstrate how to create, encode, and decode courier envelopes using the BitFoundation package.
Creating and Sealing an Envelope
import BitFoundation
// Prepare the recipient's Noise static public key
let recipientStaticKey: Data = // recipient's Noise static public key
let message: Data = "Hello, offline!".utf8
// Derive the daily rotating tag for today
let today = Date()
let tag = CourierEnvelope.recipientTag(
noiseStaticKey: recipientStaticKey,
epochDay: CourierEnvelope.epochDay(for: today)
)
// Seal the message using Noise X encryption
let sealedCiphertext = try NoiseX.seal(message, to: recipientStaticKey)
// Construct the envelope with a copy-budget of 4
let envelope = CourierEnvelope(
recipientTag: tag,
expiry: UInt64(Date().addingTimeInterval(CourierEnvelope.maxLifetimeSeconds).timeIntervalSince1970 * 1000),
ciphertext: sealedCiphertext,
copies: 4
)
// Encode for BLE transmission
guard let encoded = envelope.encode() else {
fatalError("Failed to encode envelope")
}
// `encoded` can now be handed to a courier via BLE advertisement
Receiving and Verifying Envelopes
// Check if this envelope is addressed to you
let possibleTags = CourierEnvelope.candidateTags(
noiseStaticKey: myStaticKey,
around: Date()
)
if possibleTags.contains(envelope.recipientTag) {
// Verify expiration before attempting decryption
if !received.isExpired {
let plaintext = try NoiseX.open(
received.ciphertext,
withStaticKey: myStaticKey
)
print(String(data: plaintext, encoding: .utf8)!)
}
}
Security Guarantees
According to the WHITEPAPER.md and source implementation, the courier service provides four critical security properties:
- Confidentiality: Couriers possess only the encrypted ciphertext and rotating tags; message content remains inaccessible without the recipient's private keys.
- Forward Secrecy: Envelopes can optionally be sealed to one-time pre-keys identified by
prekeyID, limiting exposure if static keys are compromised later. - Replay Resistance: The combination of
maxLifetimeSecondsand halving copy-budgets prevents infinite message replication or delayed replay attacks. - Metadata Reduction: Daily rotating recipient tags prevent long-term correlation of communication patterns, while the spray-and-wait protocol avoids centralized traffic analysis.
Summary
- The BitChat courier store-and-forward service enables offline messaging by having devices physically carry encrypted envelopes for mutual favorites.
- Envelope structure is defined in
CourierEnvelope.swiftas a TLV containing rotating tags, expiry timestamps, ciphertext, and copy-budgets. - Privacy is protected through
recipientTag(noiseStaticKey:epochDay:)andcandidateTags(noiseStaticKey:around:)which generate daily rotating tags that cannot be linked across days. - Anti-amplification is enforced by halving the
copiesfield (max 8) at each hop, while expiration is handled byisExpired(at:)using the 24-hourmaxLifetimeSecondsconstant. - Bridge couriers extend reach to Nostr relays without compromising confidentiality, as implemented in
BridgeCourierServiceand tested inBridgeCourierServiceTests.swift.
Frequently Asked Questions
How does BitChat prevent couriers from reading stored messages?
Couriers receive only the opaque courier envelope, which contains end-to-end encrypted ciphertext sealed with the recipient's Noise static key. The envelope format in CourierEnvelope.swift ensures that without the recipient's private key, the payload remains indistinguishable from random data, providing confidentiality even if the courier device is compromised.
What prevents a malicious courier from copying and flooding the network with my envelope?
Each envelope contains a copies field initialized to a maximum of 8. According to the spray-and-wait protocol implementation, the copy-budget is halved each time the envelope is forwarded to a new courier. Once the budget reaches zero, the envelope stops spreading, preventing amplification attacks while still allowing sufficient diffusion through a crowd to reach the recipient.
How long does BitChat retain undelivered courier envelopes?
By default, envelopes expire after 24 hours as defined by the maxLifetimeSeconds constant. The isExpired(at:) method checks the timestamp embedded during envelope creation. Additionally, the MessageRouter's outbox logic discards envelopes once three distinct couriers have accepted them, balancing reliability with storage constraints.
Can offline messages be delivered over the internet instead of just BLE mesh?
Yes, through the bridge courier mechanism implemented in BridgeCourierService. Devices acting as bridges can deposit opaque courier envelopes onto Nostr relays when no local BLE path exists. The bridge cannot decrypt the payload, maintaining end-to-end security while extending delivery beyond direct radio range, as verified in bitchatTests/Services/BridgeCourierServiceTests.swift.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →