# How Peer Identity and Peer ID Rotation Work in BitChat: A Technical Deep Dive

> Explore how BitChat uses unforgeable peer IDs derived from Noise static public keys for secure rotation. Learn about fresh cryptographic material and signed v2 announce for privacy. Discover BitChat peer identity and rotation.

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

---

**BitChat binds every device to an unforgeable 8-byte peer ID derived from its Noise static public key, requiring fresh cryptographic material and a signed v2 announce to achieve true rotation without privacy leaks.**

BitChat's peer identity system centers on cryptographic binding between a short identifier and a device's long-term Noise key. This design, documented in the permissionlesstech/bitchat repository, ensures that peer IDs cannot be spoofed or arbitrarily changed without rotating the underlying static key pair. Understanding this mechanism is essential for developers working with the protocol's handshake, storage, and messaging layers.

## Peer ID Derivation and Cryptographic Binding

Every BitChat device is identified by an **8-byte peer ID** computed as the first 8 bytes of the SHA-256 hash of its Noise static public key:

```swift
let peerID = SHA256(noiseStaticPublicKey)[0..<8]

```

This derivation appears throughout the codebase and creates an **unforgeable link** between the identifier and the cryptographic key. Two critical components enforce this binding:

- **`BLEPeerRegistry`** — validates that an advertised peer ID matches the derived value from the provided static key (PEER-ID-ROTATION.md lines 62-68)
- **`NoiseSessionManager`** — throws `NoiseSessionError.peerIdentityMismatch` when the recomputed ID differs from the expected value (lines 82-84)

Because derivation is deterministic, any party can verify a peer ID's legitimacy given the static public key. This eliminates spoofing attacks where a malicious peer might claim another device's ID.

## Why Simple Rotation Fails: The Replacement Binding Requirement

A device cannot achieve privacy-preserving rotation by merely changing its peer ID while retaining the same static keys. If the announce still carries identical Noise and Ed25519 keys, the derived-ID check immediately links the new ID to the previous identity (PEER-ID-ROTATION.md lines 39-45).

**True rotation requires simultaneous replacement** of:
- The static Noise key pair (source of peer ID derivation)
- The Ed25519 signing key
- The peer ID itself

This **replacement binding** is advertised through a v2 announce structure that carries both the new static key and the rotated ID, signed to prevent replay attacks.

## The v2 Announce and Rotation Mechanics

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

The rotating peer ID (`peerID_e`) derives from a long-lived rotation secret `K_rot` and the current time epoch:

```swift
let epoch: UInt32 = UInt32(Date().timeIntervalSince1970 / 86400)  // days
let msg = Data("bitchat-peer-id-v2".utf8) + epoch.bigEndian

let peerID_e = HMAC<SHA256>.authenticationCode(
    for: msg,
    using: SymmetricKey(data: K_rot)
).prefix(8)

```

This produces a time-bounded identifier that changes with each epoch without requiring new cryptographic material (PEER-ID-ROTATION.md lines 106-110).

### Step 2: Build and Sign the Announce

The v2 announce binds the rotating ID to the new static keys through a canonical signature:

```swift
let signature = NoiseEncryptionService.buildAnnounceSignature(
    context: "bitchat-announce-v1",
    peerID: peerID_e,
    noiseKey: newStaticNoiseKey,      // NEW key pair
    ed25519Key: newSigningKey,        // NEW signing key
    nickname: nickname,
    timestampMs: timestamp
)

let announce = BLEAnnounce(
    peerID: peerID_e,
    noiseStaticKey: newStaticNoiseKey,
    signingKey: newSigningKey,
    nickname: nickname,
    timestamp: timestamp,
    signature: signature
)

```

The signature covers the exact announce contents, preventing adversaries from replaying old announces or substituting values (lines 142-148).

### Step 3: Receiver Verification

Upon receipt, the verifying peer:

1. Recomputes `peerID_e` from `K_rot` and the announced epoch
2. Validates the Ed25519 signature against the advertised static keys
3. Accepts the new binding, replacing any previous `(peerID → static key)` mapping

Failure at any step results in `NoiseSessionError.peerIdentityMismatch`.

### Step 4: State Migration

During transition periods, `MessageRouter.peerIDAliases` and `ChatPeerIdentityCoordinator.migrateChatState` collapse old and new IDs into unified entries, preventing duplicate peer displays in the UI (lines 249-254).

## System-Wide Impact of Peer ID Rotation

Rotation affects multiple subsystems that must migrate from peer-ID-based to fingerprint-based addressing:

| Component | Required Adaptation |
|-----------|-------------------|
| **Noise handshake** | `NoiseSessionManager` must derive IDs from the *new* static key; lines 1106-1122 require updates (lines 259-261) |
| **Message outbox** | Pending DMs keyed by peer ID become orphaned; storage must use stable fingerprint instead (lines 261-263) |
| **Initiator tie-break** | The comparison `localPeerID < peerID` for role determination needs a stable value—rotating IDs would flip ordering mid-negotiation (lines 263-264) |
| **Prefix caches** | `SecureIdentityStateManager`, `ChatGroupCoordinator`, and similar caches using `peerID` prefixes must switch to static key fingerprints (lines 264-266) |
| **Gossip archive** | Historical packets retain original sender IDs; queries spanning rotation events need migration logic (lines 275-277) |

## Capability Advertisement

BitChat uses a `UInt64 OptionSet` for capability discovery. **Bit 14** (`peerIDRotation`) signals support for rotating IDs, allowing legacy clients to detect and appropriately handle v2 announces (lines 237-244). This capability is checked in [`PeerCapabilities.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerCapabilities.swift) before attempting rotation-specific protocol features.

## Complete Verification Example

```swift
import CryptoKit

func verifyRotatingAnnounce(
    _ announce: BLEAnnounce,
    rotationSecret: Data
) throws -> VerifiedPeerBinding {
    // Reconstruct expected rotating ID
    let expectedID = HMAC<SHA256>.authenticationCode(
        for: Data("bitchat-peer-id-v2".utf8) + announce.epoch.bigEndian,
        using: SymmetricKey(data: rotationSecret)
    ).prefix(8)
    
    guard expectedID == announce.peerID else {
        throw NoiseSessionError.peerIdentityMismatch
    }
    
    // Verify signature binds ID to new static keys
    try NoiseEncryptionService.verifyAnnounceSignature(
        context: "bitchat-announce-v1",
        peerID: announce.peerID,
        noiseKey: announce.noiseStaticKey,
        ed25519Key: announce.signingKey,
        nickname: announce.nickname,
        timestampMs: announce.timestamp,
        signature: announce.signature
    )
    
    // Derive new stable peer ID from new static key
    let newStableID = SHA256.hash(data: announce.noiseStaticKey).prefix(8)
    
    return VerifiedPeerBinding(
        rotatingID: announce.peerID,
        stableID: Data(newStableID),
        staticKey: announce.noiseStaticKey,
        signingKey: announce.signingKey
    )
}

```

## Key Implementation Files

| File | Responsibility |
|------|----------------|
| [`docs/PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md) | Full protocol design, threat model, and migration checklist |
| [`bitchat/Noise/NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseSessionManager.swift) | Identity mismatch detection and handshake enforcement |
| [`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) | `peerIDRotation` capability bit definition |
| [`bitchatTests/Services/BLEAnnounceHandlingPolicyTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/BLEAnnounceHandlingPolicyTests.swift) | Announce parsing and rejection tests |
| [`bitchatTests/ChatPeerIdentityCoordinatorContextTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/ChatPeerIdentityCoordinatorContextTests.swift) | v1-to-v2 ID migration scenario tests |

## Summary

- **Peer ID derivation**: Deterministic 8-byte prefix of SHA-256(static Noise key), unforgeable and universally verifiable
- **Rotation requirement**: Privacy-preserving rotation demands *both* a new rotating ID *and* new static cryptographic material
- **v2 announce structure**: HMAC-based epoch ID + fresh static keys + canonical signature prevents replay and linking attacks
- **System migration**: Storage, handshake, and caching layers must adopt stable fingerprints as primary keys to support rotation

## Frequently Asked Questions

### Can a BitChat peer change its ID without generating new keys?

No. Because the peer ID derives directly from the static Noise public key, any ID change requires rotating that key. Attempting to advertise a different ID with the same static key causes `NoiseSessionError.peerIdentityMismatch` in `NoiseSessionManager`. The protocol explicitly rejects this scenario as it provides no privacy benefit—the old and new IDs remain cryptographically linked.

### What prevents replay attacks on v2 rotation announces?

The `buildAnnounceSignature` function creates a binding signature over the canonical announce bytes, including the rotating ID, new static Noise key, Ed25519 key, nickname, and timestamp. This ties each announce to a specific cryptographic state and time window. Receivers verify this signature before accepting any ID replacement, ensuring attackers cannot replay old announces to regress a peer's identity.

### How do legacy clients handle peers with rotating IDs?

The `peerIDRotation` capability bit (bit 14) in the `PeerCapabilities` OptionSet allows new clients to advertise support. Legacy clients without this bit should ignore v2 announce fields they don't recognize, treating the peer as a standard v1 peer. The protocol maintains backward compatibility through gradual capability negotiation rather than hard protocol version requirements.

### Why use HMAC-SHA256 instead of direct SHA-256 for the rotating ID?

The rotating ID `peerID_e` must change predictably across epochs without revealing the underlying static key. HMAC with a long-lived secret `K_rot` provides this: the same secret produces deterministic IDs for peers who share it, while the irreversibility of HMAC protects `K_rot` from extraction. Direct SHA-256 of a public value would be guessable; HMAC requires the shared secret to compute or verify.