How Peer ID Rotation in BitChat Works: Identity Management and Privacy Considerations
Peer ID rotation in BitChat replaces static 8‑byte identifiers derived from SHA‑256 hashes of Noise public keys with time‑based rotating IDs computed via HKDF‑SHA256, enabling unlinkability while preserving mutual recognition through pairwise tags and cryptographically signed binding messages.
BitChat’s original Bluetooth Low Energy (BLE) protocol exposes devices to long‑term tracking by binding the peer ID to the static hash of a Noise key pair (peerID == SHA‑256(noiseStaticPublicKey)[0…8]). The permissionlesstech/bitchat repository implements a privacy‑first rotation scheme that cryptographically severs this link, preventing passive observers from correlating broadcasts across time while maintaining seamless discovery for trusted contacts.
What Is Peer ID Rotation in BitChat?
In the legacy protocol, every device broadcasts an immutable 8‑byte identifier for its entire lifetime. This allows adversaries to track protest attendees or users across physical locations by passively sniffing BLE advertisements. Peer ID rotation addresses this by deriving temporary identifiers from the private Noise key using a time‑based epoch system.
The rotation scheme is governed by six design goals codified in docs/PEER-ID-ROTATION.md:
- G1 – Unlinkability: Prevent passive listeners from correlating observations of the same device across rotation periods.
- G2 – Confidentiality: Eliminate broadcast leakage of long‑term public keys or nicknames.
- G3 – Seamless Recognition: Allow mutual favourites to auto‑recognise each other without repeating handshakes.
- G4 – Discoverability: Preserve the ability for strangers to discover and handshake with devices.
- G5 – Mixed‑Version Interoperability: Support gradual rollout without flag days.
- G6 – No Extra Spoofing Risk: Maintain existing security baselines against identity spoofing.
Cryptographic Primitives for Rotating Identities
All cryptographic operations reside in localPackages/BitFoundation/Sources/BitFoundation/PeerIDRotation.swift as pure, deterministic Swift code with no I/O dependencies.
Epoch Calculation and Windowing
Rotating IDs change every rotation period (default 1 hour). The system calculates the current epoch as floor(unixTime / ROTATION_PERIOD) and uses a ±1 window (candidateEpochs(around:)) to absorb clock skew and boundary crossings.
let epoch = PeerIDRotation.epoch(at: Date())
let candidates = PeerIDRotation.candidateEpochs(around: Date()) // [epoch-1, epoch, epoch+1]
Generating the Rotating Peer ID
Rather than hashing the public key, the peer ID derives from the private Noise key using HKDF‑SHA256 with info string "bitchat-peer-rotation-v1". An HMAC‑SHA256 with context "bitchat-peer-id-v2" and the epoch produces the final 8‑byte identifier:
let rotationSecret = PeerIDRotation.rotationSecret(noiseStaticPrivateKey)
let peerID = PeerIDRotation.peerID(
rotationSecret: rotationSecret,
epoch: currentEpoch) // 8 bytes
Pairwise Recognition Tags for Mutual Favourites
Because rotating IDs are unlinkable across epochs, mutual favourites cannot rely on the peer ID alone for recognition. Instead, they compute directional recognition tags using a shared X25519 secret.
The Recognition Key and Tag
For each mutual favourite pair, the system derives a recognitionKey via HKDF‑SHA256 over the X25519 shared secret (info "bitchat-recognition-v1"). The tag binds the current peer ID, epoch, and ordered public keys:
let tag = PeerIDRotation.recognitionTag(
recognitionKey: PeerIDRotation.recognitionKey(sharedSecret: sharedSecret),
epoch: epoch,
senderStaticPublicKey: myNoisePublic,
recipientStaticPublicKey: friendNoisePublic,
peerID: myPeerID) // 8‑byte Data
Tag Block Structure
Announce packets carry up to eight recognition tags in a fixed 64‑byte tag block (TAG_SLOTS = 8). Real tags are shuffled and padded with random bytes so the block size reveals nothing about the number of mutual favourites.
let matches = PeerIDRotation.blockMatches(
tagBlock,
recognitionKey: recognitionKey,
senderStaticPublicKey: senderNoisePub,
recipientStaticPublicKey: myNoisePub,
peerID: senderPeerID,
at: Date())
Binding Messages and Handshake Verification
Since rotating peer IDs are no longer derivable from static Noise keys, legacy pre‑flight checks (derivedPeerID == peerID) fail. The protocol replaces these with an explicit signed binding transmitted during the Noise handshake.
Binding Message Format
The binding message concatenates a version string, epoch, rotating peer ID, and the static Noise public key:
"bitchat-peerid-binding-v1" || uint32be(epoch) || peerID || noiseStaticPublicKey
Verification Flow
Inside NoiseSessionManager.swift (lines 1106‑1122), the peer signs this binding with its Ed25519 identity key after completing the Noise handshake. Verification confirms that the rotating ID presented in the BLE advertisement legitimately belongs to the static Noise key used for the session, preventing spoofing attacks.
Integration Points in the BitChat Codebase
Implementing peer ID rotation requires modifications across the networking and storage layers:
BLEAnnounceHandlingPolicy.swift: Replaces the static derivation check at lines 32‑35 withannounceV2 = 0x2Cparsing and tag‑matching logic.NoiseSessionManager.swift: Defers identity verification until the binding message signature is validated post‑handshake.MessageOutboxStore.swift: Re‑keys queued messages by stable fingerprint rather than rotating peer ID to prevent data loss during rotation events.BLEFragmentAssemblyBuffer.swift: Ensures fragment reassembly completes before rotation or uses consistent IDs for in‑flight packets to avoid stranding.PeerCapabilities.swift: Defines capability bit 14 (peerIDRotation) to signal v2 support.MeshTopologyTracker.swift: Records observed protocol versions and applies version‑gated routing logic inrecordObservedVersion(_:for:).
Rollout Strategy and Compatibility
The migration proceeds through three phases to maintain network connectivity:
- Parse‑Only Phase: Clients parse
announceV2and advertise the capability bit but continue sending v1 announces. - Dual‑Announce Phase: Devices broadcast both v1 and v2 announces (alternating). New clients prefer v2 handshakes while legacy clients remain compatible.
- v1‑Sunset Phase: After sufficient telemetry confirms adoption, v1 announces are suppressed by default. Pure v2 devices become invisible to old clients—a deliberate privacy trade‑off.
Code Examples
Generate the Current Rotating Peer ID
import BitFoundation
let noisePrivKey: Data = // 32‑byte private key from keychain
let now = Date()
let peerID = PeerIDRotation.currentPeerID(
noiseStaticPrivateKey: noisePrivKey,
at: now) // Returns 8‑byte Data
Create a Recognition Tag for a Mutual Favourite
let sharedSecret: Data = // X25519 shared secret
let recognitionKey = PeerIDRotation.recognitionKey(sharedSecret: sharedSecret)
let tag = PeerIDRotation.recognitionTag(
recognitionKey: recognitionKey,
epoch: PeerIDRotation.epoch(at: now),
senderStaticPublicKey: myNoisePublic,
recipientStaticPublicKey: friendNoisePublic,
peerID: myPeerID)
Verify an Incoming Tag Block
let tagBlock: Data = // 64‑byte block from BLE advertisement
let isRecognised = PeerIDRotation.blockMatches(
tagBlock,
recognitionKey: recognitionKey,
senderStaticPublicKey: senderNoisePub,
recipientStaticPublicKey: myNoisePub,
peerID: senderPeerID,
at: Date())
Construct the Binding Message for Handshake
let binding = PeerIDRotation.bindingMessage(
epoch: PeerIDRotation.epoch(at: Date()),
peerID: myPeerID,
noiseStaticPublicKey: myNoisePublic)
// Sign `binding` with Ed25519 and transmit in AuthenticatedPeerStatePacket
Summary
- Peer ID rotation cryptographically decouples device identity from long‑term public keys, requiring replacement of static‑hash verification with explicit signed bindings.
- Recognition tags enable mutual favourites to identify each other across rotation periods without leaking relationship metadata to passive observers.
- Protocol version negotiation via capability bit 14 and
announceV2 = 0x2Censures backward compatibility during the phased rollout. - Storage and routing layers must migrate from 8‑byte peer ID keys to stable fingerprints to prevent message loss during rotation events.
- All core functions are implemented in
PeerIDRotation.swiftwith pure Swift cryptography, eliminating clock and I/O dependencies.
Frequently Asked Questions
How does peer ID rotation prevent tracking in BitChat?
By deriving the 8‑byte broadcast identifier from the private Noise key using HKDF‑SHA256 and a time‑based epoch (default 1 hour), the protocol ensures that passive listeners cannot link different advertisements to the same physical device across rotation windows. This eliminates the long‑term correlation surface that existed when the peer ID was a static SHA‑256 hash of the public key.
How do mutual favourites recognise each other after ID rotation?
Mutual favourites pre‑compute directional recognition tags using the X25519 shared secret and the current epoch. These tags are embedded in a shuffled, padded 64‑byte block within the BLE advertisement. When a device receives an advertisement, it tests the tag block against its locally stored recognition keys; a match confirms the sender is a known contact without revealing the relationship to eavesdroppers.
What changes are required to support rotating peer IDs in BitChat?
Implementers must update BLEAnnounceHandlingPolicy.swift to accept announceV2 messages and verify recognition tags instead of static key hashes. NoiseSessionManager.swift must verify the Ed25519 signature on the binding message post‑handshake. Additionally, MessageOutboxStore.swift and BLEFragmentAssemblyBuffer.swift require updates to index by stable fingerprints rather than rotating peer IDs to ensure message delivery persists across epoch boundaries.
How does the phased rollout maintain compatibility with older clients?
The rollout uses three phases: parse‑only (understand v2 but send v1), dual‑announce (alternate v1 and v2 broadcasts), and v1‑sunset (suppress v1 once adoption is sufficient). During the dual‑announce phase, new clients prefer v2 handshakes while legacy clients continue operating via v1, ensuring the mesh remains connected during the transition.
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 →