# How Bitchat Handles User Authentication: Noise XX Handshake and Cryptographic Identity

> Discover how Bitchat handles user authentication with Noise XX handshake, creating password-less cryptographic identities stored securely in your system keychain.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-06

---

**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](https://github.com/permissionlesstech/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

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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.

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/SecureNoiseSession.swift) for session lifecycle enforcement.*

## Complete Authentication Flow Example

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/SecureIdentityStateManager.swift) | Encrypted identity storage, signing key binding, verification tracking |
| [`NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseSessionManager.swift) | Noise XX handshake orchestration, session establishment |
| [`SecureNoiseSession.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SecureNoiseSession.swift) | Per-session encryption, message limits, renegotiation triggers |
| [`BitchatPeer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BitchatPeer.swift) | Peer identifier model (`PeerID`, fingerprint derivation) |
| [`KeychainManagerProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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 `PeerID`s 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.