# How Peer ID Rotation Enhances Privacy in the BitChat Mesh Network

> Discover how peer ID rotation boosts privacy in BitChat's mesh network. Learn how changing identifiers hourly thwarts tracking while enabling trusted contact recognition.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-03

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/PeerIDRotation.swift), the rotation logic centers on four key operations:

```swift
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:

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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

```swift
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

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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.