How Bitchat Handles User Authentication: Noise XX Handshake and Cryptographic Identity
Bitchat authenticates peers using the Noise XX handshake protocol, creating password-less, device-bound cryptographic identities that are encrypted and stored in the system keychain.
The bitchat messenger implements a zero-trust peer-to-peer authentication model. Instead of passwords or centralized accounts, each device generates cryptographic keys that mutually authenticate through the Noise protocol. This article explains the complete authentication flow, from static key generation to secure session management.
Static Key Pair Generation on Each Device
Bitchat creates a unique Ed25519 signing key and Curve25519 static key pair during installation. These keys form the foundation of all authentication.
The Curve25519 static key's public component derives the device's PeerID—a persistent identifier that remains consistent across network reconnections. The Ed25519 key handles message signing and identity binding after initial authentication completes.
This approach eliminates user-managed credentials. Security depends entirely on key material generated and protected by the device itself.
Noise XX Handshake for Mutual Authentication
When two Bitchat devices discover each other, they establish trust through a three-message Noise XX handshake coordinated by NoiseSessionManager.
Handshake Message Flow
- Initiator calls
initiateHandshake(with:)and sends Noise message 1 - Responder processes via
handleIncomingHandshake(from:message:)and replies with messages 2 and 3 - Both parties derive shared secrets and verify static public keys
// Initiate a handshake (simplified)
let payload = try noiseSessionManager.initiateHandshake(with: remotePeerID)
// Process an inbound handshake packet
if let response = try noiseSessionManager.handleIncomingHandshake(
from: remotePeerID,
message: incomingData
) {
// send `response` back to the remote peer
}
The authenticatedRemoteKey property in NoiseSessionManager.handleIncomingHandshake validates that the remote static public key cryptographically matches the claimed PeerID. Only when isEstablished() returns true has authentication succeeded.
See NoiseSessionManager.swift for the complete implementation.
Binding Signing Keys After Authentication
Successful Noise authentication enables a second layer of identity binding. When a peer announces an Ed25519 signing key, SecureIdentityStateManager.bindAuthenticatedSigningPublicKey pins that key to the authenticated fingerprint.
identityManager.bindAuthenticatedSigningPublicKey(signingKeyData,
fingerprint: fingerprint)
The fingerprint is the SHA-256 hash of the Noise static key. Once bound, subsequent attempts to replace the pinned signing key are rejected. This prevents impersonation attacks where a malicious actor might try to substitute their own signing key for an already-authenticated peer.
See SecureIdentityStateManager.swift for key binding logic.
Encrypted Persistence in the System Keychain
All identity state resides in an in-memory IdentityCache with encrypted, synchronous persistence:
- Encryption: AES-GCM with a key stored in the iOS/macOS Keychain
- Atomic writes: Barrier queue ensures
saveIdentityCache()completes before returning - Race condition protection:
pendingSaveflag prevents concurrent write attempts
private func saveIdentityCache() {
pendingSave = true
persist(snapshot: cache) // encrypt + store in Keychain
pendingSave = false
}
This design guarantees that fingerprint-to-public-key mappings, verification flags, and social graph data survive app termination while remaining inaccessible to other processes.
See SecureIdentityStateManager.swift for persistence implementation.
Verification State and Trust Decisions
Bitchat tracks two trust signals independently:
| Method | Purpose |
|---|---|
setVerified(_:forFingerprint:) |
Explicit user confirmation of a peer's identity |
recordVouch(fromFingerprint:forTargetFingerprint:) |
Transitive trust through mutual connections |
UI components consult isVerified(fingerprint:) and effectiveTrustLevel(for:) to determine display treatment and message handling policies. This separates cryptographic authentication (guaranteed by Noise) from social trust (explicit user judgment).
Session Limits and Cryptographic Renewal
SecureNoiseSession enforces hard limits on authenticated sessions:
- Maximum message count: Prevents key exhaustion attacks
- Session age timeout: Forces periodic re-authentication
When limits approach, needsRenegotiation() triggers a fresh handshake:
if noiseSession.needsRenegotiation() {
try noiseSessionManager.initiateReconnectHandshake(with: peerID, ...)
}
Old session keys are discarded. New handshakes re-verify static keys against stored fingerprints, maintaining continuous authentication without user intervention.
See SecureNoiseSession.swift for session lifecycle enforcement.
Complete Authentication Flow Example
// 1️⃣ Create managers (once, at app start)
let keychain = KeychainManager() // conforms to KeychainManagerProtocol
let identityManager = SecureIdentityStateManager(keychain)
let noiseManager = NoiseSessionManager(
localStaticKey: Curve25519.KeyAgreement.PrivateKey(),
keychain: keychain
)
// 2️⃣ Start handshake with a discovered peer
let handshakePayload = try noiseManager.initiateHandshake(with: remotePeerID)
// 3️⃣ Send `handshakePayload` over BLE / TCP …
// 4️⃣ When a packet arrives from the peer:
let result = try noiseManager.handleIncomingHandshake(
from: remotePeerID,
message: inboundData
)
// 5️⃣ If the session became established, bind the signing key that may be
// included in the peer's announce (e.g. via a Nostr or BLE announce):
if result.didEstablishAuthenticatedSession,
let signingKey = announcedSigningKeyData {
let fingerprint = remotePeerID.fingerprint // SHA-256 of static key
identityManager.bindAuthenticatedSigningPublicKey(signingKey,
fingerprint: fingerprint)
}
// 6️⃣ Verify a peer later:
let isTrusted = identityManager.isVerified(fingerprint: remotePeerID.fingerprint)
Core Authentication Files
| File | Responsibility |
|---|---|
SecureIdentityStateManager.swift |
Encrypted identity storage, signing key binding, verification tracking |
NoiseSessionManager.swift |
Noise XX handshake orchestration, session establishment |
SecureNoiseSession.swift |
Per-session encryption, message limits, renegotiation triggers |
BitchatPeer.swift |
Peer identifier model (PeerID, fingerprint derivation) |
KeychainManagerProtocol.swift |
Keychain abstraction for AES-GCM key storage |
Summary
- Noise XX handshake provides mutual, password-less authentication without centralized servers
- Static Curve25519 keys derive persistent
PeerIDs and fingerprints for every device - Signing key binding prevents impersonation through fingerprint-pinned Ed25519 keys
- Keychain-encrypted storage protects identity mappings with hardware-backed AES-GCM
- Session limits and renewal enforce cryptographic hygiene through automatic re-handshakes
Frequently Asked Questions
Does Bitchat use passwords for authentication?
No. Bitchat uses cryptographic key pairs generated automatically during installation. The Noise XX handshake protocol authenticates peers by verifying static public keys, eliminating the need for user-managed passwords entirely.
How does Bitchat prevent someone from impersonating a trusted contact?
After the initial Noise handshake, Bitchat pins the peer's Ed25519 signing key to their fingerprint in SecureIdentityStateManager. The bindAuthenticatedSigningPublicKey method rejects any subsequent attempts to bind a different key to the same fingerprint, blocking impersonation attempts.
What happens if a Bitchat session is compromised?
Sessions self-destruct. SecureNoiseSession enforces maximum message counts and time limits. When needsRenegotiation() triggers, both parties discard old keys and perform a fresh Noise handshake, re-verifying each other's static keys against stored fingerprints.
Where are Bitchat's cryptographic keys stored?
Noise static keys and the AES-GCM encryption key for the identity cache reside in the iOS/macOS Keychain. The in-memory IdentityCache is the only decrypted representation, synchronized through a barrier queue to prevent data races during persistence.
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 →