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

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, the identifier is derived using SHA-256:

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

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

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

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


Key Files in the Rotation Implementation

File Role in Rotation
docs/PEER-ID-ROTATION.md Complete design rationale, migration checklist, and security analysis
bitchat/Noise/NoiseSessionManager.swift Identity derivation enforcement and peerIdentityMismatch errors
bitchat/Noise/NoiseEncryptionService.swift buildAnnounceSignature and verifyAnnounceSignature helpers
bitchat/PeerCapabilities.swift Capability bit definitions including peerIDRotation
bitchatTests/Services/BLEAnnounceHandlingPolicyTests.swift Unit tests for announce parsing and ID mismatch rejection
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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →