How BitChat Derives Ephemeral Cryptographic Identities: A Deep Dive into the NostrIdentityBridge
BitChat derives ephemeral cryptographic identities deterministically from a single device-wide seed using HMAC-SHA256 with geohash inputs, ensuring each location channel has an unlinkable, temporary Nostr-compatible key pair that is never persisted beyond the session.
The permissionlesstech/bitchat repository implements a privacy-preserving messaging protocol where users interact through location-based channels without exposing long-term cryptographic identities. Instead of storing static private keys, BitChat generates ephemeral cryptographic identities on-the-fly using a deterministic derivation scheme that maintains unlinkability between different geohash cells.
The Foundation: Device-Wide Seed Generation
Every ephemeral identity in BitChat originates from a single 256-bit random seed generated once per device. In bitchat/Nostr/NostrIdentityBridge.swift, the method getOrCreateDeviceSeed() creates and persists this seed in the system keychain under the identifier nostr-device-seed.
The seed is cached in memory after first retrieval to avoid repeated keychain operations. This cached seed serves as the root secret from which all location-specific identities are mathematically derived. If the device seed is wiped or lost—for example, through a factory reset—all previously derived ephemeral identities become permanently unrecoverable, breaking any linkability across device wipes.
Per-Location Identity Derivation via HMAC-SHA256
For each geohash location (e.g., "u4pruydq"), BitChat creates a unique identity through a deterministic HMAC-SHA256 process implemented in deriveIdentity(forGeohash:). The derivation uses the device seed as the HMAC key and the UTF-8 bytes of the geohash string concatenated with an iteration counter as the message.
The resulting 32-byte digest becomes candidate private-key material for a secp256k1 Schnorr key pair. This deterministic approach ensures that the same device always generates the identical ephemeral identity for the same location, enabling message continuity without requiring persistent storage of per-location keys.
The Derivation Loop and secp256k1 Validation
The candidate bytes produced by the HMAC operation are not guaranteed to form a valid secp256k1 private key. BitChat addresses this through a validation loop:
- Compute HMAC-SHA256(seed, geohash + counter)
- Attempt initialization via
NostrIdentity(privateKeyData:) - If the bytes fail secp256k1 validation, increment the counter (0-9) and retry
- Return the first valid identity encountered
This retry mechanism ensures cryptographic correctness while maintaining deterministic output for any valid geohash input.
Deterministic Fallback Mechanism
After ten failed attempts, BitChat falls back to a deterministic alternative. The system computes SHA-256(seed || geohash), hashes the result once more, and interprets the final digest as the private key. This fallback guarantees that every geohash maps to exactly one valid identity, eliminating edge cases where HMAC outputs consistently fail validation.
Caching and Thread Safety
To optimize performance during UI rendering, successfully derived NostrIdentity objects are stored in an in-memory dictionary named derivedIdentityCache. This cache uses the geohash string as the key and is protected by an NSLock to ensure thread-safe access across concurrent operations.
The caching layer eliminates redundant HMAC computations and keychain access when the user rapidly switches between location channels. Since the cache resides solely in memory, it provides no persistence—ephemeral identities vanish when the application terminates.
Bridge Rendezvous Isolation
BitChat supports bridge rendezvous points for hybrid Bluetooth/Nostr messaging. To prevent correlation attacks between location identities and bridge identities, the system implements deriveIdentity(forBridgeRendezvous:) which prepends the static label "bridge|" to the cell string before derivation.
This namespace separation ensures that a bridge identity for "cell-42" cannot be mathematically linked to a geohash identity, even if the underlying cell identifiers collide. The isolation maintains the unlinkability guarantees across the entire hybrid messaging architecture.
Technical Implementation in Swift
The following examples demonstrate the identity derivation patterns implemented in bitchat/Nostr/NostrIdentityBridge.swift and bitchat/Nostr/NostrIdentity.swift.
Deriving a per-geohash identity:
import BitFoundation
import BitChat
let keychain = KeychainManager() // Production keychain wrapper
let bridge = NostrIdentityBridge(keychain: keychain)
do {
// Derive an identity unique to the geohash "u4pruydq"
let geoIdentity = try bridge.deriveIdentity(forGeohash: "u4pruydq")
print("Public key (hex):", geoIdentity.publicKeyHex) // 32-byte x-only
print("Bech32 npub:", geoIdentity.npub) // Nostr-compatible
} catch {
print("Failed to derive identity:", error)
}
Deriving a bridge-rendezvous identity:
let bridgeIdentity = try bridge.deriveIdentity(forBridgeRendezvous: "cell-42")
print("Bridge npub:", bridgeIdentity.npub)
Generating the global Nostr identity for relay authentication:
let globalIdentity = try NostrIdentity.generate()
print("Global npub:", globalIdentity.npub)
All methods return a NostrIdentity struct wrapping a secp256k1 Schnorr key, providing schnorrSigningKey() for event signatures and npub for Bech32-encoded public key representation.
Summary
- BitChat creates ephemeral cryptographic identities from a single 256-bit device seed stored in the system keychain under
nostr-device-seed. - Location-specific keys are derived via HMAC-SHA256 using the geohash and an iteration counter, validated against secp256k1 requirements with a ten-attempt retry loop.
- A deterministic SHA-256 fallback ensures valid key generation even when HMAC outputs fail validation.
- The
derivedIdentityCachewithNSLockprovides thread-safe, in-memory caching without persistent storage. - Bridge rendezvous identities are isolated through a
"bridge|"namespace prefix, maintaining unlinkability across the hybrid messaging infrastructure.
Frequently Asked Questions
How does BitChat ensure ephemeral cryptographic identities are unlinkable across different locations?
BitChat derives each identity deterministically from the device seed combined with a unique geohash input. Since different geohashes produce different HMAC-SHA256 outputs, the resulting secp256k1 key pairs are mathematically unrelated. An observer monitoring multiple location channels cannot correlate the ephemeral public keys (npub values) without access to the original device seed.
What happens if the HMAC-SHA256 derivation produces an invalid secp256k1 private key?
The deriveIdentity(forGeohash:) method implements an iteration counter (0-9) that modifies the HMAC input message. If the initial candidate fails NostrIdentity(privateKeyData:) validation, the system increments the counter and recomputes. After ten failed attempts, BitChat falls back to SHA-256(SHA-256(seed || geohash)) to guarantee a valid key.
Where is the device seed stored and how is it protected?
The 256-bit seed is generated once and persisted in the iOS/macOS keychain under the identifier nostr-device-seed, implemented in NostrIdentityBridge.getOrCreateDeviceSeed(). The seed is cached in memory during application runtime to minimize keychain access, but it never persists to disk outside the secure enclave storage.
Can ephemeral identities be recovered after the device seed is lost?
No. Because ephemeral cryptographic identities are derived deterministically from the device seed using HMAC-SHA256, losing the seed permanently destroys the ability to regenerate any previously used identities. This property ensures forward secrecy across device resets—new installations generate fresh seeds with no mathematical connection to previous ephemeral identities.
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 →