# How BitChat Manages Peer ID Rotation to Enhance User Privacy

> Discover how BitChat uses hourly peer ID rotation, cryptographically derived from a static Noise private key, to prevent tracking and boost user privacy. Learn about pairwise recognition tags.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-04

---

**BitChat prevents long-term tracking by rotating an 8-byte peer identifier every hour, cryptographically derived from a static Noise private key, with known contacts finding each other through pairwise recognition tags.**

BitChat is a privacy-focused, peer-to-peer messaging application that uses Bluetooth Low Energy (BLE) for local communication. Because BLE packets are broadcast in the open, a static identifier would allow adversaries to track users over time. The `permissionlesstech/bitchat` repository implements a sophisticated **peer ID rotation** mechanism that breaks linkability while preserving discoverability for mutual contacts.

## Epoch-Based Rotation Schedule

The rotation follows a time-based epoch system defined in [`docs/PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md). Each device independently calculates the current epoch without network synchronization:

```

epoch = ⌊unixTime / ROTATION_PERIOD⌋

```

The `ROTATION_PERIOD` defaults to one hour but is configurable. To handle clock skew and boundary crossings, BitChat accepts a **±1 epoch window** (`candidateEpochs`), allowing devices with slightly divergent clocks to recognize each other.

This design eliminates coordination overhead—no central server or peer negotiation is required. Every device arrives at the same epoch value deterministically.

## Deriving the Rotation Secret

In [`localPackages/BitFoundation/Sources/BitFoundation/PeerIDRotation.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/PeerIDRotation.swift), the rotation system anchors to the device's long-term **Noise static private key**. From this key, BitChat derives a permanent rotation secret `K_rot` using HKDF-SHA256:

```swift
let rotationSecret = HKDF.derive(
    inputKeyingMaterial: noiseStaticPrivateKey,
    salt: nil,
    info: "bitchat-peer-rotation-v1".data(using: .utf8)
)

```

This secret **never leaves the device** and never rotates. Compromise of a single epoch's peer ID does not expose past or future identifiers, as each requires knowledge of `K_rot` to compute.

## Generating the Rotating Peer ID

For each epoch, BitChat generates a new 8-byte peer ID through HMAC-SHA256:

```swift
// Compute the current rotating peer ID
let privateKey = ... // Noise static private key (Data)
let peerID = PeerIDRotation.currentPeerID(
    noiseStaticPrivateKey: privateKey,
    at: Date()
)

```

The HMAC operates over the concatenation of `"bitchat-peer-id-v2"` and the epoch integer, keyed with `K_rot`. Only the **first 8 bytes** are retained. Because `K_rot` is private, observers cannot:

- Predict future peer IDs
- Link past peer IDs to the same device
- Forge valid peer IDs for any epoch

## Binding the Rotating ID to the Static Key

The original BitChat protocol bound identity directly to the static Noise key (`peerID == SHA-256(staticKey)[0..8]`). Rotation breaks this invariant. To solve this, [`PeerIDRotation.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerIDRotation.swift) implements a **cryptographic binding message** signed with the device's Ed25519 signing key:

The signed payload contains:
- The current epoch
- The rotating peer ID
- The static Noise public key

This binding proves ownership of the peer ID without exposing the static keys in BLE broadcasts. The verification occurs inside a completed Noise session, ensuring the proof is only readable by the intended recipient.

## Recognition Tags for Mutual Favourites

Peers that have previously established trust need a way to recognize each other across rotations. BitChat solves this with **pairwise recognition tags**:

```swift
// Derive a pairwise recognition tag for a mutual favourite
let sharedSecret = ... // X25519(shared secret) between the two devices
let recognitionKey = PeerIDRotation.recognitionKey(sharedSecret: sharedSecret)
let tag = PeerIDRotation.recognitionTag(
    recognitionKey: recognitionKey,
    epoch: PeerIDRotation.epoch(at: Date()),
    senderStaticPublicKey: myPublicKey,
    recipientStaticPublicKey: favouritePublicKey,
    peerID: peerID
)

```

The tag computation uses HMAC-SHA256 keyed with `K_AB`, the X25519 shared secret between two static keys. Inputs include the epoch, both public keys, and the rotating peer ID. This makes tags **directional and epoch-bound**—only the two parties can generate or verify a specific tag.

## Tag Block Padding for Metadata Protection

The `announceV2` packet carries recognition tags in a fixed-size block defined in [`PeerIDRotation.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerIDRotation.swift):

- **8 slots × 8 bytes** = 64 bytes total
- Real tags placed in **random positions**
- Empty slots filled with **uniform random bytes**

This constant-size structure prevents metadata leakage about how many mutual favourites a user has. An observer cannot distinguish between "no contacts nearby" and "eight contacts nearby" from packet inspection alone.

The `tagBlock(tags:)` function implements this padding automatically:

```swift
// Build the padded tag block for the announceV2 packet
let tagBlock = PeerIDRotation.tagBlock(tags: [tag])

```

## Compatibility and Packet Evolution

BitChat introduces the new packet type through [`AnnounceV2Packet.swift`](https://github.com/permissionlesstech/bitchat/blob/main/AnnounceV2Packet.swift):

| Packet Type | Value | Purpose |
|-------------|-------|---------|
| `announce` | `0x01` | Legacy, static peer ID (deprecated) |
| `announceV2` | `0x02` | Rotating peer ID with tag block |

During rollout, devices broadcast **both packet types** to maintain interoperability with older clients. The [`BLEAnnounceHandlingPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEAnnounceHandlingPolicy.swift) file implements validation logic that accepts both formats, with [`NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseSessionManager.swift) handling the upgraded handshake that uses signed binding messages.

## Implementation Files and Key Functions

| File | Key Functions | Responsibility |
|------|---------------|--------------|
| [`PeerIDRotation.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerIDRotation.swift) | `currentPeerID(noiseStaticPrivateKey:at:)`, `recognitionTag(recognitionKey:epoch:senderStaticPublicKey:recipientStaticPublicKey:peerID:)`, `tagBlock(tags:)` | Core rotation logic, tag generation, block padding |
| [`AnnounceV2Packet.swift`](https://github.com/permissionlesstech/bitchat/blob/main/AnnounceV2Packet.swift) | Packet encoding/decoding for `0x02` type | BLE advertisement structure |
| [`BLEAnnounceHandlingPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEAnnounceHandlingPolicy.swift) | Pre-flight validation | Incoming packet filtering |
| [`NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseSessionManager.swift) | Handshake with signed binding | Session establishment |
| [`PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/PEER-ID-ROTATION.md) | Protocol specification | Design documentation and threat model |

## Summary

- **Time-based epochs** enable synchronized rotation without coordination
- **HKDF-derived secrets** anchor rotation to long-term keys without exposing them
- **HMAC-based peer IDs** ensure unlinkability and unforgeability across epochs
- **Ed25519 binding messages** prove ID ownership without static key exposure
- **Pairwise recognition tags** let trusted contacts reconnect despite rotation
- **Fixed-size tag blocks** prevent metadata leakage about contact counts
- **Dual packet broadcast** ensures backward compatibility during deployment

## Frequently Asked Questions

### How often does BitChat rotate peer IDs?

BitChat rotates peer IDs every **hour by default**, based on unix time divided by a configurable `ROTATION_PERIOD`. The system accepts a ±1 epoch window to accommodate clock skew between devices, so effective recognition spans approximately 2–3 hours depending on boundary alignment.

### Can an observer link multiple peer IDs to the same device?

No. Peer IDs are **cryptographically unlinkable** to passive observers. Each epoch's ID requires knowledge of `K_rot`, which is derived from the private Noise key via HKDF-SHA256. Without this secret, computing past or future IDs is computationally infeasible, and the HMAC construction provides no algebraic structure to exploit.

### How do mutual contacts find each other after rotation?

Mutual contacts use **pairwise recognition tags** derived from their shared X25519 secret. Each tag is epoch-specific and directional, computed with HMAC-SHA256 over the epoch, both public keys, and the current peer ID. Only the two parties can generate matching tags, allowing recognition without revealing identity to third parties.

### What prevents metadata leakage about a user's number of contacts?

The tag block in `announceV2` packets is **fixed at 64 bytes** (8 slots × 8 bytes) regardless of how many recognition tags are present. Real tags occupy random positions, and empty slots contain indistinguishable random padding. This constant-size design prevents observers from inferring contact count from packet inspection.