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 conditiondeviceLocked— temporary state, retry warrantedaccessDenied— permission failure, likely permanentduplicateItem— collision handling neededstorageFull— 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):
- Verify the install-lifecycle gate is open
- Prefix keys with
"identity_"for namespacing - Route through generic save/load paths
- 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 —
KeychainManagerProtocolenables testability and platform abstraction - Reinstall detection —
KeychainInstallAccessGateprevents stale key reuse via hidden lifecycle markers - Background-ready accessibility —
AfterFirstUnlockThisDeviceOnlysupports locked-device operation - Precise error taxonomy —
KeychainSaveResult/KeychainReadResultenable state-appropriate responses - Memory sanitization —
memset_sclearing 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →