How Peer Identity and Peer ID Rotation Work in BitChat: A Technical Deep Dive
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:
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— throwsNoiseSessionError.peerIdentityMismatchwhen 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:
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:
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:
- Recomputes
peerID_efromK_rotand the announced epoch - Validates the Ed25519 signature against the advertised static keys
- 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 before attempting rotation-specific protocol features.
Complete Verification Example
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 |
Full protocol design, threat model, and migration checklist |
bitchat/Noise/NoiseSessionManager.swift |
Identity mismatch detection and handshake enforcement |
bitchat/Noise/NoiseEncryptionService.swift |
buildAnnounceSignature and verifyAnnounceSignature helpers |
bitchat/PeerCapabilities.swift |
peerIDRotation capability bit definition |
bitchatTests/Services/BLEAnnounceHandlingPolicyTests.swift |
Announce parsing and rejection tests |
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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →