How Peer ID Rotation Enhances Privacy in the BitChat Mesh Network
Peer ID rotation makes each device appear as a completely new, random node every hour by deriving fresh identifiers from a time-based epoch and static Noise key, breaking long-term correlation attacks while letting trusted contacts recognize each other through encrypted recognition tags.
In permissionlesstech/bitchat, the rotating Peer ID system replaces traditional static identifiers with cryptographically ephemeral identities. This design enables BitChat to operate as a zero-knowledge mesh network where passive observers cannot track device movement or communication patterns over time. According to the source specification in docs/PEER-ID-ROTATION.md, the rotation mechanism combines deterministic epoch calculation with per-device secrets to produce unlinkable yet reproducible identifiers.
Breaking Linkability with Ephemeral Peer IDs
The core privacy guarantee comes from time-bound identifier rotation. Instead of broadcasting a single, permanent Peer ID, BitChat devices compute a new identifier every epoch (default 3,600 seconds).
Epoch-Based ID Derivation
In localPackages/BitFoundation/Sources/BitFoundation/PeerIDRotation.swift, the rotation logic centers on four key operations:
public enum PeerIDRotation {
// Maps timestamp to 32-bit epoch counter
public static func epoch(at date: Date) -> UInt32
// Generates tolerant epoch window for clock drift
public static func candidateEpochs(around date: Date) -> [UInt32]
// Derives rotation secret from static Noise private key
public static func rotationSecret(noiseStaticPrivateKey: Data) -> Data
// Produces the rotating Peer ID for a specific epoch
public static func peerID(rotationSecret: Data, epoch: UInt32) -> Data
}
The peerID(rotationSecret:epoch:) function at line 102 combines the device's persistent rotation secret with the current epoch number through a cryptographic hash. The output is a fixed-length opaque byte string that appears entirely random to anyone lacking the derivation parameters.
This means an observer watching network traffic at 09:00 and 10:00 sees two completely unrelated identifiers—even though both originate from the same physical device.
Selective Continuity Through Recognition Tags
Rotating IDs would break user experience if contacts had to re-authenticate hourly. BitChat solves this with mutual favourite recognition tags that preserve continuity without compromising privacy.
How Recognition Tags Work
When two peers mark each other as favourites, they exchange a shared secret through their existing Noise session. From this secret, both parties derive:
// Line 127 - Static key material for tag generation
public static func recognitionKey(sharedSecret: Data) -> Data
// Line 159 - Ephemeral tag valid only for specific epoch
public static func recognitionTag(_ sharedSecret: Data,
epoch: UInt32,
recognitionKey: Data) -> Data
The recognitionTag function produces a short, epoch-specific value that proves identity without revealing the underlying static keys. These tags are bundled into a tag block carried in AnnounceV2Packet broadcasts.
Key properties of this design (from docs/PEER-ID-ROTATION.md line 47):
- Tags are short-lived — valid only for the current epoch
- Tags are non-transferable — derived from pairwise secrets, unusable by third parties
- Recognition requires prior relationship — strangers cannot link rotated IDs
Stateless Discovery Without Static Key Exposure
BitChat's discovery protocol never places static identifying material on the wire. The rotating Peer ID appears in two critical packet types:
| Packet | Field | Purpose |
|---|---|---|
AuthenticatedPeerStatePacket |
Rotating Peer ID + encrypted nickname | Initial authenticated introduction |
AnnounceV2Packet |
Rotating Peer ID + tag block | Ongoing presence announcement |
As noted in the design spec at line 249, BitChat supports dual-ID operation during protocol upgrades—devices simultaneously advertise both stable v1 and rotating v2 identifiers to maintain backward compatibility while enabling privacy.
Threats Mitigated by Peer ID Rotation
The implementation directly counters several surveillance techniques common in mesh networks:
Long-term correlation attacks Static identifiers allow adversaries to build activity profiles over days or weeks. Hourly rotation forces attackers to solve a cryptographic linking problem for every epoch—computationally infeasible at scale.
Passive network mapping Relays and boundary observers see continuously changing node populations. Without recognition tag secrets, the mesh appears as an ever-shifting collection of unrelated identities rather than a stable graph of communicants.
Metadata leakage via nicknames
Nicknames move inside encrypted AuthenticatedPeerStatePacket payloads per the BRING_THE_NOISE design. Cleartext broadcast of human-readable handles—a common linkage vector in other systems—is eliminated.
Replay attacks
Each epoch generates a fresh bindingMessage signed with epoch-specific material. Packets from previous epochs fail verification, preventing replay of captured announcements.
Targeted tracking Even if an adversary knows a target's static key, they cannot predict or recognize future rotating IDs without the shared secret required for tag generation.
Practical Implementation Examples
Generating the Current Rotating Peer ID
import BitFoundation
// Retrieve from secure enclave or keychain
let noiseStaticPrivateKey: Data = secureStorage.loadNoiseKey()
// Current epoch's identifier for packet construction
let currentID = PeerIDRotation.currentPeerID(
noiseStaticPrivateKey: noiseStaticPrivateKey,
at: Date()
)
// Use in AnnounceV2Packet construction
let announce = AnnounceV2Packet(
peerID: currentID,
tagBlock: buildTagBlock(for: currentEpoch)
)
Verifying Incoming Recognition Tags
func authenticatePeer(
packet: AnnounceV2Packet,
knownContacts: [Contact]
) -> PeerAuthenticationResult {
let currentEpoch = PeerIDRotation.epoch(at: Date())
for contact in knownContacts {
let expectedTag = PeerIDRotation.recognitionTag(
contact.sharedSecret,
epoch: currentEpoch,
recognitionKey: PeerIDRotation.recognitionKey(sharedSecret: contact.sharedSecret)
)
if PeerIDRotation.blockMatches(packet.tagBlock, recognitionKey: expectedTag) {
return .recognized(contact, authenticatedAs: packet.peerID)
}
}
return .unknown(packet.peerID)
}
Key Source Files and Specifications
| Path | Role | Critical Lines |
|---|---|---|
docs/PEER-ID-ROTATION.md |
Protocol specification and privacy rationale | Epoch definition (L11), test vectors (L13), mutual favourite handling (L47), dual-ID operation (L249) |
localPackages/BitFoundation/Sources/BitFoundation/PeerIDRotation.swift |
Core rotation implementation | epoch() (L71), candidateEpochs() (L82), rotationSecret() (L92), peerID() (L102), currentPeerID() (L113), recognitionKey() (L127), recognitionTag() (L159) |
localPackages/BitFoundation/Tests/BitFoundationTests/PeerIDRotationTests.swift |
Executable test vectors and regression prevention | Rotation period verification, epoch mathematics, tag generation correctness |
localPackages/BitFoundation/Sources/BitFoundation/AnnounceV2Packet.swift |
Broadcast packet structure | Tag block placement and rotating ID field (L47) |
Summary
- Peer ID rotation generates fresh, unlinkable identifiers every hour from static Noise keys and epoch counters
- Linkability breaks because rotated IDs appear cryptographically random without the derivation secret
- Recognition tags enable trusted contacts to identify each other across rotations without exposing static keys
- Stateless discovery places only ephemeral material on the wire—no persistent identifiers broadcast
- Multiple attack vectors are mitigated: long-term correlation, passive surveillance, metadata leakage, replay, and targeting
Frequently Asked Questions
How often does BitChat rotate Peer IDs?
BitChat rotates Peer IDs every 3,600 seconds (one hour) by default. The epoch(at:) function in PeerIDRotation.swift maps timestamps to 32-bit epoch counters using integer division of Unix time by the rotation period. This interval balances privacy (frequent changes) with practical considerations like clock drift tolerance and recognition tag overhead.
Can two mutual favourites find each other after a rotation without re-handshaking?
Yes. Peers that have previously authenticated exchange a recognition tag derived from their shared secret. This tag, generated by recognitionTag(sharedSecret:epoch:recognitionKey:) and validated via blockMatches(), proves identity across epochs without requiring a new Noise handshake or revealing static keys. Only peers with established relationships can perform this linking.
What prevents an attacker from linking my rotated Peer IDs?
The critical barrier is the rotation secret—a cryptographically secure value derived from your device's static Noise private key through rotationSecret(noiseStaticPrivateKey:). Without this secret, which never leaves your device, rotated Peer IDs are indistinguishable from random data. The derivation function acts as a one-way hash, making prediction or reversal computationally infeasible.
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 →