How Peer Identity Is Managed in Bitchat: Noise Static Keys and Persistent Identifiers
Bitchat manages peer identity through a cryptographically bound dual-identifier system derived from a persistent Noise static key, using a full 64-character public key for authentication and a 16-character SHA-256 fingerprint for efficient routing and discovery.
In the permissionlesstech/bitchat repository, peer identity management centers on the Noise static key pair generated once per device and stored in the system keychain. The protocol derives two forms of identifiers from this key: a full 32-byte public key for cryptographic handshakes and a stable 8-byte short ID embedded in every BLE packet header.
The Dual-Identifier Architecture
Bitchat uses two distinct representations of the same underlying identity to balance security with efficiency.
Full Noise Public Key
The full Noise public key is a 64-hex-character string representing 32 bytes of Curve25519 key-agreement data. Stored in the device keychain after initial generation, this key serves as the ultimate source of truth for peer authentication. It appears directly in cryptographic handshakes and signature-binding operations, but remains hidden from passive network observers.
Persistent Short Peer ID
The persistent short peer ID is a 16-hex-character (8-byte) fingerprint derived deterministically from the static public key. Implemented in PeerID.swift, this identifier is computed by taking the SHA-256 hash of the Noise public key and extracting the first 8 bytes (16 hex characters). This short ID is embedded in every BLE packet header and used for routing, neighbour discovery, and peer-list management.
Deriving the Persistent Identifier from the Noise Static Key
When the application needs a stable identifier for display or routing, it calls the PeerID initializer with the raw public key data. The derivation occurs in two stages across separate source files.
First, Data+SHA256.swift provides the hashing implementation:
// Data+SHA256.swift (lines 13-17)
extension Data {
func sha256Fingerprint() -> String {
// Returns full SHA-256 hash as hex string
}
}
Then, PeerID.swift truncates this to create the short identifier:
// PeerID.swift (lines 31-34)
public init(publicKey: Data) {
self.init(str: publicKey.sha256Fingerprint().prefix(16))
}
This design ensures the short ID is cryptographically bound to the static key—any change to the underlying key automatically produces a different fingerprint, breaking existing peer associations.
Identity Verification Workflows
Bitchat verifies peer identity at two critical stages: when receiving BLE announcements and during the Noise XX handshake.
BLE Announce Pre-flight Checks
When a device receives a BLE announce packet, BLEAnnounceHandlingPolicy.swift validates that the advertised peer ID matches the hash of the announced static key. This prevents spoofing where a malicious actor claims another peer's short ID.
// BLEAnnounceHandlingPolicy.swift (lines 32-35)
let derivedPeerID = PeerID(publicKey: announcement.noisePublicKey)
guard derivedPeerID == peerID else {
return .reject(.senderMismatch(derivedPeerID: derivedPeerID))
}
If the derived ID does not match the packet header, the implementation returns a senderMismatch rejection, dropping the connection attempt before cryptographic resources are expended.
Noise Handshake Authentication
During the Noise XX handshake, NoiseSessionManager.swift validates the remote static key against the claimed peer ID through the authenticatedRemoteKey method:
// NoiseSessionManager.swift (lines 31-41)
private func authenticatedRemoteKey(_ remoteKey: Curve25519.KeyAgreement.PublicKey,
matches claimedPeerID: PeerID) -> Bool {
let rawKey = remoteKey.rawRepresentation
if claimedPeerID.isShort {
return PeerID(publicKey: rawKey) == claimedPeerID
}
if let claimedNoiseKey = claimedPeerID.noiseKey {
return claimedNoiseKey == rawKey
}
return false
}
This function handles both short ID and full public key comparisons, ensuring the remote party possesses the private key corresponding to the claimed identity.
Security and Persistence Guarantees
Because the short peer ID is a fingerprint rather than the key itself, passive observers cannot reconstruct the full 32-byte public key from the 16-character identifier alone. The identifier remains persistent across app restarts and device reboots as long as the Noise static key remains unchanged in the keychain.
This persistence provides stable routing tables without requiring a centralized identity server. However, if a device rotates its static key—documented in the repository's PEER-ID-ROTATION.md design document—the derived short ID changes automatically, forcing a re-authentication with all previously known peers.
Summary
- Dual identifiers: Bitchat uses a full 64-hex Noise public key for authentication and a 16-hex short ID for routing.
- Derivation: The short ID is computed as
SHA256(staticKey).prefix(16)inPeerID.swiftusing utilities fromData+SHA256.swift. - Verification:
BLEAnnounceHandlingPolicy.swiftvalidates BLE announces against derived IDs, whileNoiseSessionManager.swiftauthenticates handshake keys. - Binding: The short ID is cryptographically bound to the static key; any rotation changes the identifier and breaks existing associations.
- Storage: The static key persists in the device keychain, providing a stable identity across sessions.
Frequently Asked Questions
How is the short peer ID generated from the Noise static key?
The short peer ID is generated by computing the SHA-256 hash of the 32-byte Curve25519 public key and extracting the first 8 bytes (16 hex characters). This occurs in PeerID.swift through the sha256Fingerprint() method defined in Data+SHA256.swift.
Why does Bitchat use a shortened identifier instead of the full public key?
The short ID reduces packet overhead in BLE announcements and routing tables while preserving cryptographic binding. The full 64-character key is used only during authenticated handshakes, keeping the public key hidden from passive observers and reducing the attack surface for reconnaissance.
What happens if a peer's Noise static key is compromised or rotated?
If the static key is rotated, the SHA-256 fingerprint changes, automatically generating a new short peer ID. According to the PEER-ID-ROTATION.md design document, this breaks all existing peer associations, requiring re-authentication with the new key pair.
How does Bitchat prevent peer ID spoofing during BLE discovery?
The BLEAnnounceHandlingPolicy.swift implementation derives the expected peer ID from the announced Noise public key and compares it to the ID claimed in the packet header. If they mismatch, the packet is rejected with a senderMismatch error before the handshake begins, preventing spoofing attacks.
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 →