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

  1. Initiator calls initiateHandshake(with:) and sends Noise message 1
  2. Responder processes via handleIncomingHandshake(from:message:) and replies with messages 2 and 3
  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: pendingSave flag 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:

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 →