# How Peer Identity Rotation Works in BitChat: Cryptographic Design and Trigger Conditions

> Discover how BitChat peer identity rotation generates new IDs and fresh keys. Learn about trigger conditions like user requests or epoch expiry.

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

---

**BitChat peer identity rotation generates a new 8-byte identifier from a rotating secret and announces it alongside a fresh Noise static key pair, triggered whenever a user requests ID change or when the current epoch expires (default 24 hours).**

BitChat uses **peer-ID rotation** to improve metadata privacy in mesh messaging. Unlike static identifiers that permanently tie a device to its network traffic, rotating IDs break linkability over time. This article explains the cryptographic mechanism, the conditions that trigger rotation, and how the protocol maintains security while updating identity across the stack.

---

## How BitChat Derives the Base Peer Identity

Every BitChat device is identified by a short **8-byte peer ID** that is cryptographically bound to its long-term keys. This binding prevents spoofing and forms the foundation for the rotation mechanism.

In [`bitchat/Noise/NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseSessionManager.swift), the identifier is derived using SHA-256:

```swift
import CryptoKit

func derivePeerID(from staticKey: Data) -> Data {
    // SHA‑256 of the static public key, first 8 bytes
    let hash = SHA256.hash(data: staticKey)
    return Data(hash.prefix(8))
}

```

This derivation is **verified in two critical locations**:

- `BLEPeerRegistry` validates the derived ID against the advertised value
- `NoiseSessionManager` throws `NoiseSessionError.peerIdentityMismatch` when the check fails

Because the peer ID is **tightly bound** to the static Noise key, a device cannot simply rotate its ID without also rotating the underlying cryptographic material. This constraint shapes the entire rotation design.

---

## Trigger Conditions for Peer Identity Rotation

According to the [`docs/PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md) specification, rotation is triggered by two primary conditions:

1. **Explicit user request** — a manual action in the UI to "rotate identity"
2. **Epoch expiration** — automatic rotation when the current epoch (default: 24-hour windows) elapses

The epoch-based trigger ensures that even without user intervention, linkability degrades over time. The current epoch is computed as:

```swift
let epoch = UInt32(Date().timeIntervalSince1970 / 86400)  // days since Unix epoch

```

Both triggers execute the same rotation procedure: generate new cryptographic material, derive a rotating ID for the current epoch, and announce the replacement binding to the network.

---

## The Rotation Mechanics: From Static to Rotating Identity

When rotation is triggered, BitChat executes a **four-step protocol** documented in [`docs/PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md) sections 4.2 and 4.5.

### Step 1: Generate the Epoch-Based Rotating ID

A secret `K_rot` (long-lived rotation key) and the current epoch produce a temporary identifier via HMAC-SHA256:

```swift
let rotSecret = SymmetricKey(data: K_rot)
let msg = Data("bitchat-peer-id-v2".utf8) + epoch.bigEndian
let peerID_e = HMAC<SHA256>.authenticationCode(for: msg, using: rotSecret).prefix(8)

```

This `peerID_e` is the **rotating identifier** visible on the network during this epoch.

### Step 2: Create and Sign the v2 Announce

The rotating ID is useless without binding it to authenticated cryptographic material. The announce packet carries:

- The rotating ID (`peerID_e`)
- A **new** static Noise key pair (the replacement)
- The Ed25519 signing key
- Timestamp and nickname

```swift
let announce = BLEAnnounce(
    peerID: peerID_e,
    noiseStaticKey: newStaticKey,
    signingKey: ed25519Key,
    nickname: myNickname,
    timestamp: Date().millisecondsSince1970
)

let signature = NoiseEncryptionService.buildAnnounceSignature(
    context: "bitchat-announce-v1",
    peerID: peerID_e,
    noiseKey: newStaticKey,
    ed25519Key: ed25519Key,
    nickname: myNickname,
    timestampMs: announce.timestamp
)

```

The **context string and signature** prevent replay attacks and bind the rotating ID to this specific announce.

### Step 3: Verify on Receipt

Receivers recompute `peerID_e` from the shared rotation secret and validate:

```swift
func verifyAnnounce(_ announce: BLEAnnounce) throws {
    let expectedPeerID = HMAC<SHA256>.authenticationCode(
        for: Data("bitchat-peer-id-v2".utf8) + announce.epochBigEndian,
        using: SymmetricKey(data: K_rot)
    ).prefix(8)
    
    guard expectedPeerID == announce.peerID else {
        throw NoiseSessionError.peerIdentityMismatch
    }
    
    try NoiseEncryptionService.verifyAnnounceSignature(
        context: "bitchat-announce-v1",
        announce: announce
    )
}

```

### Step 4: Migrate State to the New Identity

Because the old ID may still exist in caches and message queues, `MessageRouter.peerIDAliases` and `ChatPeerIdentityCoordinator.migrateChatState` **collapse duplicate entries** so the UI presents a single continuous conversation.

---

## Why Rotation Requires Key Replacement

A critical insight from the BitChat design: **rotating only the peer ID provides no privacy benefit**.

If the same static Noise keys appear in announces after "rotation," the derived-ID check (`SHA256(staticKey)[0..<8]`) immediately links the new ID to the old device. The protocol therefore requires **simultaneous rotation of both ID and static key pair**.

This replacement binding ensures that:
- Network observers cannot correlate epochs for the same device
- Cryptographic sessions with old peers terminate cleanly
- New sessions use fresh forward-secrecy guarantees

---

## System-Wide Impact of Peer Identity Rotation

Peer-ID rotation touches nearly every component of the BitChat stack. The [`docs/PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md) document outlines these impacts:

| Component | Required Adaptation |
|-----------|-------------------|
| **Noise session handshake** | `NoiseSessionManager` lines 1106-1122 must derive ID from the *new* static key |
| **Message outbox** | Queued DMs keyed by peer ID require re-keying by stable fingerprint to avoid orphaned messages |
| **Initiator tie-break** | `localPeerID < peerID` comparison needs stable key (e.g., fingerprint) since rotating IDs flip ordering |
| **Cache lookups** | `SecureIdentityStateManager` and `ChatGroupCoordinator` must use static key or fingerprint instead of `peerID` prefixes |
| **Gossip archive** | Historic packets retain old sender IDs; queries spanning rotation events need migration logic |

These adaptations ensure that rotation improves privacy **without breaking functionality**.

---

## Capability Advertisement for Backward Compatibility

BitChat encodes peer capabilities in a `UInt64` `OptionSet`. **Bit 14** is reserved for `peerIDRotation`, allowing:

- New clients to detect rotating-ID support in peers
- Old clients to gracefully handle v1 and v2 announces
- Gradual network upgrade without partitioning

This capability flag is defined in [`bitchat/PeerCapabilities.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/PeerCapabilities.swift).

---

## Key Files in the Rotation Implementation

| File | Role in Rotation |
|------|------------------|
| [`docs/PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md) | Complete design rationale, migration checklist, and security analysis |
| [`bitchat/Noise/NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseSessionManager.swift) | Identity derivation enforcement and `peerIdentityMismatch` errors |
| [`bitchat/Noise/NoiseEncryptionService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseEncryptionService.swift) | `buildAnnounceSignature` and `verifyAnnounceSignature` helpers |
| [`bitchat/PeerCapabilities.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/PeerCapabilities.swift) | Capability bit definitions including `peerIDRotation` |
| [`bitchatTests/Services/BLEAnnounceHandlingPolicyTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/BLEAnnounceHandlingPolicyTests.swift) | Unit tests for announce parsing and ID mismatch rejection |
| [`bitchatTests/ChatPeerIdentityCoordinatorContextTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/ChatPeerIdentityCoordinatorContextTests.swift) | State migration tests for v1-to-v2 identity transitions |

---

## Summary

- **Peer identity rotation in BitChat** combines epoch-based temporary IDs with cryptographic key replacement to break linkability
- **Trigger conditions** include explicit user action and automatic epoch expiration (24-hour windows)
- **The rotating ID (`peerID_e`)** is derived via HMAC-SHA256 from a long-lived secret and current epoch
- **Replacement binding** requires announcing both the new ID and new static Noise keys; rotation alone is insufficient
- **System-wide changes** maintain conversation continuity through `peerIDAliases` and migrate storage keys to stable fingerprints

---

## Frequently Asked Questions

### How often does automatic peer identity rotation occur in BitChat?

According to [`docs/PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md), automatic rotation triggers on **epoch boundaries** defined as 24-hour windows (86400 seconds). The epoch is computed as `UInt32(Date().timeIntervalSince1970 / 86400)`, so rotation occurs at most once per day unless manually triggered sooner.

### Can BitChat peers with rotating IDs communicate with older non-rotating peers?

Yes, through **capability advertisement**. Bit 14 in the `UInt64` `OptionSet` indicates `peerIDRotation` support. Old clients without this capability continue using the static derived ID from `SHA256(noiseStaticKey)[0..<8]`, while new clients handle both v1 (static) and v2 (rotating) announces.

### What happens to pending messages when a peer's identity rotates?

Without intervention, queued direct messages would become **orphaned** because the outbox keys messages by peer ID. The design requires re-keying the outbox by **stable fingerprint** (the hash of long-term keys) rather than the rotating ID, ensuring messages reach the correct recipient post-rotation.

### Why does BitChat require new static keys instead of just rotating the peer ID display?

Because the **base peer ID is derived from the static Noise key**. If only the display ID rotated while `noiseStaticKey` remained constant, any peer could recompute `SHA256(oldStaticKey)[0..<8]` and immediately link the "new" rotating ID to the original device. True metadata privacy requires **joint rotation** of both identifier and cryptographic material.