How Ephemeral Keys Enable Location-Based Privacy in BitChat
BitChat uses ephemeral keys derived per-geohash to ensure that public keys cannot be linked across different location cells, preventing long-term tracking while still enabling real-time presence detection.
BitChat, an open-source location-based messaging app from the permissionlesstech/bitchat repository, solves the privacy challenge of broadcasting location signals by never reusing a single static identity. Instead, the protocol generates ephemeral keys that are cryptographically bound to specific geohash cells, ensuring that activity in different regions appears as distinct, unlinkable identities to Nostr relays.
Per-Geohash Ephemeral Identity Derivation
Cryptographic Binding to Location Cells
When a device joins a geohash-based channel, BitChat derives a temporary key pair unique to that specific geohash. According to the Geohash Presence Specification documented in /docs/GeohashPresenceSpec.md (lines 31-34), the event structure includes a pubkey that is "the ephemeral identity derived specifically for this geohash." This same derived key signs both regular chat messages (kind 20000) and presence heartbeat events (kind 20001).
Containing Linkability Within Single Cells
Because each geohash precision level receives its own key pair, an observer monitoring the Nostr relay cannot correlate activity across different regions, cities, or neighborhoods. The public keys visible for a region-level geohash bear no cryptographic relationship to those used for city-level or block-level cells, confining linkability to the boundaries of a single geohash.
Ephemeral Presence Events and Broadcast Randomization
Kind 20001 Ephemeral Heartbeats
BitChat marks presence events as explicitly ephemeral using Nostr kind 20001, which relays handle as non-persistent events per the specification in /docs/GeohashPresenceSpec.md (lines 9-13). These events are not stored long-term by relays, leaving minimal forensic evidence for tracking analysis.
Randomized Timing Intervals
To prevent temporal correlation attacks, GeohashViewModel.swift implements a global heartbeat coroutine that broadcasts presence at randomized intervals. As specified in /docs/GeohashPresenceSpec.md (lines 41-49), the system waits between 40 and 80 seconds between broadcasts, injecting additional per-message random delays of 2 to 5 seconds. This jitter breaks predictable patterns that could otherwise link simultaneous broadcasts across different geohash precision levels.
Precision-Based Privacy Filtering
The protocol explicitly disables presence broadcasting for high-precision geohashes. Per the specification (lines 45-48), heartbeats are only sent for low-precision levels (region, province, city), while neighborhood, block, and building levels are excluded from presence announcements, preventing fine-grained location disclosure.
Unified Participant Counting Without Identity Exposure
The Participant Map Mechanism
GeohashMessageHandler.swift maintains an in-memory map of pubkey → lastSeenTimestamp for both chat events (kind 20000) and presence events (kind 20001). A participant is considered online if seen within the last 5 minutes, as documented in /docs/GeohashPresenceSpec.md (lines 60-66). This approach counts silent listeners and active speakers alike while respecting the per-geohash ephemerality.
UI Integration and Intentional Uncertainty
LocationChannelsModel.swift supplies the user interface with participant counts, applying the "?[people]" placeholder for high-precision channels where presence is disabled. As noted in the specification (lines 71-76), this communicates the intentional uncertainty to users, reinforcing the privacy-by-design approach.
Implementation Example
The Swift implementation demonstrates how these components work together:
// 1️⃣ Derive the ephemeral key for the current geohash
let geoKey = NostrProtocol.ephemeralKey(for: currentGeohash)
// 2️⃣ Create a presence heartbeat event (kind 20001)
let presence = NostrProtocol.createGeohashPresenceEvent(
geohash: currentGeohash,
signingKey: geoKey
)
// 3️⃣ Broadcast the event on all allowed geohash channels
await NostrRelay.publish(event: presence)
// 4️⃣ Update the participant map on receipt of any presence or chat event
func handle(event: NostrEvent) {
guard event.kind == .geohashChat || event.kind == .geohashPresence else { return }
participantMap[event.pubkey] = Date()
}
The NostrProtocol.ephemeralKey(for:) function derives the temporary key pair bound to the supplied geohash, while NostrProtocol.createGeohashPresenceEvent constructs the signed, empty-content event of kind 20001. The GeohashViewModel.startGlobalPresenceHeartbeat() method manages the randomized loop that triggers these broadcasts whenever the app is active.
Summary
- Ephemeral keys are derived per-geohash in BitChat, ensuring that each location cell uses a distinct, unlinkable identity.
- Presence events use ephemeral kind 20001 and are not stored long-term by relays, minimizing data retention for tracking.
- Randomized broadcast intervals (40-80 seconds with 2-5 second jitter) prevent temporal correlation across different geohash levels.
- Precision filtering restricts presence broadcasts to region, province, and city levels only, hiding exact coordinates.
- Unified participant counting in
GeohashMessageHandler.swifttracks activity within a 5-minute window without requiring persistent identities.
Frequently Asked Questions
What are ephemeral keys in BitChat?
Ephemeral keys in BitChat are temporary cryptographic key pairs generated specifically for a single geohash cell. Unlike static identities, these keys are derived on-demand using NostrProtocol.ephemeralKey(for:) and are used exclusively for events within that specific location boundary, preventing long-term tracking of users across different areas.
How does BitChat prevent linking a user's activity across different locations?
BitChat generates a unique ephemeral key for every geohash precision level (region, city, etc.), ensuring that the public key appearing in a regional channel bears no cryptographic relationship to the key used in a city channel. As implemented in the permissionlesstech/bitchat codebase, this containment strategy limits linkability to single geohash cells only.
Why are presence events considered ephemeral in the Nostr protocol?
BitChat uses Nostr kind 20001 for presence heartbeats, which is classified as an ephemeral event kind that relays do not store permanently according to the Geohash Presence Specification. This ephemeral classification means presence data disappears from relays after propagation, leaving no persistent record that could be analyzed for movement patterns.
How can BitChat show participant counts without revealing who is online?
The GeohashMessageHandler updates a map of ephemeral public keys to last-seen timestamps for both chat and presence events, considering a participant "online" if seen within the last 5 minutes. Because these are ephemeral keys that cannot be linked to real identities or other locations, the system displays approximate counts (like "?[people]" for high-precision channels) without exposing individual user data or exact locations.
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 →