BitChat Keychain Encryption Key Management Explained

BitChat stores all cryptographic material in the iOS/macOS keychain through a dedicated KeychainManager service that enforces strict lifecycle gating, device-locked accessibility, and secure memory clearing.

BitChat is a permissionless messaging application that relies on robust encryption key management to protect user identities and conversations. The project's keychain architecture, implemented in permissionlesstech/bitchat, demonstrates production-grade patterns for handling cryptographic secrets across app lifecycles, reinstallations, and background operation.

Core Protocol Design

All keychain operations in BitChat flow through KeychainManagerProtocol, which defines the foundational contract for cryptographic storage.

Located at localPackages/BitFoundation/Sources/BitFoundation/KeychainManagerProtocol.swift, this protocol declares:

  • Primitive operations for saving, loading, and deleting keychain items
  • Higher-level helpers for identity-key management
  • Rich result types that classify keychain errors precisely

The protocol-based design enables seamless swapping between production keychain storage and test doubles, as evidenced by MockKey.swift and PreviewKeychainManager.swift implementations.

Install-Lifecycle Safeguards

BitChat implements a critical reinstall protection mechanism to prevent key reuse attacks.

The KeychainInstallAccessGate (iOS only) records a hidden install-lifecycle marker in the keychain on first launch. When the app detects a reinstall—indicated by a fresh user defaults state but an existing marker—it triggers deleteAllKeychainData() to purge stale secrets before they can be accessed.

This gate, enforced in bitchat/Services/KeychainManager.swift lines 18–33, guarantees that no cryptographic operation proceeds until marker reconciliation succeeds.

Accessibility Configuration for Background Operation

All keychain items use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, allowing BitChat to read identity keys while the device remains locked.

This accessibility level serves two critical use cases:

  • Background BLE wake-up — the app can respond to nearby peers
  • Push notification decryption — messages arrive and decrypt without user interaction

A one-time migration runs at startup to update any legacy items created with WhenUnlocked semantics, ensuring consistent behavior across updates.

Error-Rich API for Robust Handling

Save and read operations return strongly-typed results rather than throwing generic errors.

The KeychainSaveResult and KeychainReadResult enums, defined at lines 59–86 of KeychainManagerProtocol.swift, distinguish:

  • itemNotFound — legitimate absence vs. error condition
  • deviceLocked — temporary state, retry warranted
  • accessDenied — permission failure, likely permanent
  • duplicateItem — collision handling needed
  • storageFull — system resource exhaustion

This granularity lets callers implement precise retry strategies and user-facing messaging.

Identity-Key Helper Methods

The KeychainManager provides dedicated helpers for cryptographic identity material.

Methods like saveIdentityKey, getIdentityKey, and deleteIdentityKey in bitchat/Services/KeychainManager.swift (lines 60–80):

  1. Verify the install-lifecycle gate is open
  2. Prefix keys with "identity_" for namespacing
  3. Route through generic save/load paths
  4. Return the rich result types for caller interpretation

Generic Data Storage API

Beyond identity keys, BitChat supports arbitrary payload storage under service names.

The save(_:data:service:accessible:), load(_:service:), and delete(_:service:) APIs—lines 60–98 of KeychainManager.swift—handle the full keychain lifecycle while respecting accessibility and error classification rules. Services like chat.bitchat.nostr isolate different cryptographic contexts.

Secure Memory Handling

When keys are no longer needed, BitChat actively clears memory rather than relying on ARC deallocation.

Lines 33–41 of KeychainManager.swift implement memset_s zero-filling before object discard, mitigating the risk that residual secrets linger in unallocated RAM or core dumps.

Panic Mode and Complete Wipe

For user-initiated account deletion or emergency scenarios, deleteAllKeychainData() enumerates every declared service and removes all items.

This panic mode, implemented in lines 68–94, then recreates the install marker so future launches detect a clean state—preventing the reinstall-detection logic from triggering inappropriately.

Practical Usage Examples

import BitFoundation

// Obtain the shared manager (uses in-memory keychain during testing)
let keychain = KeychainManager.makeDefault()

// Save a Noise static key with proper error handling
let noiseKey = Data([0x01, 0x02, 0x03, 0x04, ...])
let saveResult = keychain.saveIdentityKeyWithResult(noiseKey, forKey: "noiseStaticKey")

switch saveResult {
case .success:
    print("Key saved successfully")
case .deviceLocked:
    // Schedule retry when device unlocks
    scheduleBackgroundRetry(forKey: "noiseStaticKey")
case .accessDenied:
    fatalError("Keychain access denied—check entitlements")
default:
    logCriticalError("Unexpected keychain failure: \(saveResult)")
}

// Retrieve with state-aware handling
let readResult = keychain.getIdentityKeyWithResult(forKey: "noiseStaticKey")

switch readResult {
case .success(let keyData):
    initializeNoiseProtocol(with: keyData)
case .itemNotFound:
    // First launch or post-wipe state
    generateAndSaveNewIdentity()
case .deviceLocked:
    // Defer operation until unlock
    registerForUnlockNotification()
case .storageFull:
    alertUserStorageExhausted()
default:
    reportToCrashlytics(readResult)
}

// Secure deletion during logout
let deleted = keychain.deleteIdentityKey(forKey: "noiseStaticKey")
assert(deleted, "Failed to purge identity key from keychain")

Summary

  • Protocol-driven architecture — KeychainManagerProtocol enables testability and platform abstraction
  • Reinstall detection — KeychainInstallAccessGate prevents stale key reuse via hidden lifecycle markers
  • Background-ready accessibility — AfterFirstUnlockThisDeviceOnly supports locked-device operation
  • Precise error taxonomy — KeychainSaveResult/KeychainReadResult enable state-appropriate responses
  • Memory sanitization — memset_s clearing prevents RAM-resident secret leakage
  • Complete wipe capability — deleteAllKeychainData() supports account deletion and emergency cleanup

Frequently Asked Questions

What accessibility level does BitChat use for keychain items?

BitChat uses kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly for all cryptographic material. This setting allows key retrieval while the device remains locked—essential for background Bluetooth Low Energy operation—but restricts key portability to other devices via backup restoration, enhancing security.

How does BitChat handle app reinstalls to prevent key confusion?

The KeychainInstallAccessGate writes a hidden marker on first install. If the app detects fresh user defaults alongside an existing marker, it concludes a reinstall occurred and invokes deleteAllKeychainData() before any cryptographic operation proceeds. This prevents stale keys from being incorrectly associated with a new installation.

Can BitChat read keys when the device is completely locked?

No—AfterFirstUnlockThisDeviceOnly requires at least one post-boot unlock. Before that first unlock, keychain items are inaccessible. This represents a deliberate trade-off between background functionality and cold-boot security.

What happens when the keychain returns deviceLocked during a read?

Callers should defer the operation and register for unlock notifications or schedule a background retry. The KeychainManager returns this specific case to distinguish temporary unavailability from permanent failures like accessDenied or itemNotFound.

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 →