# How Peer ID Rotation in BitChat Works: Identity Management and Privacy Considerations

> Explore BitChat peer ID rotation for enhanced privacy and identity management. Discover how rotating IDs ensure unlinkability while maintaining recognition through secure binding.

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

---

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

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

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

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

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/BLEAnnounceHandlingPolicy.swift)**: Replaces the static derivation check at lines 32‑35 with `announceV2 = 0x2C` parsing and tag‑matching logic.
- **[`NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseSessionManager.swift)**: Defers identity verification until the binding message signature is validated post‑handshake.
- **[`MessageOutboxStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageOutboxStore.swift)**: Re‑keys queued messages by stable fingerprint rather than rotating peer ID to prevent data loss during rotation events.
- **[`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift)**: Ensures fragment reassembly completes before rotation or uses consistent IDs for in‑flight packets to avoid stranding.
- **[`PeerCapabilities.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerCapabilities.swift)**: Defines capability **bit 14** (`peerIDRotation`) to signal v2 support.
- **[`MeshTopologyTracker.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MeshTopologyTracker.swift)**: Records observed protocol versions and applies version‑gated routing logic in `recordObservedVersion(_:for:)`.

## Rollout Strategy and Compatibility

The migration proceeds through three phases to maintain network connectivity:

1. **Parse‑Only Phase**: Clients parse `announceV2` and advertise the capability bit but continue sending v1 announces.
2. **Dual‑Announce Phase**: Devices broadcast both v1 and v2 announces (alternating). New clients prefer v2 handshakes while legacy clients remain compatible.
3. **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

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

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

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

```swift
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 = 0x2C` ensures 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.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerIDRotation.swift) with 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`](https://github.com/permissionlesstech/bitchat/blob/main/BLEAnnounceHandlingPolicy.swift) to accept `announceV2` messages and verify recognition tags instead of static key hashes. [`NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseSessionManager.swift) must verify the Ed25519 signature on the binding message post‑handshake. Additionally, [`MessageOutboxStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageOutboxStore.swift) and [`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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.