How the Panic Emergency Wipe (Triple-Tap) and Identity Rotation Work in Bitchat
The panic emergency wipe in bitchat is triggered by a triple-tap on the microphone button, which immediately aborts active audio capture, increments a generation counter to invalidate pending callbacks, and invokes synchronized wipe methods across LocationStateManager, BLEIncomingFileStore, and ChatViewModel to erase all persisted location data, media files, and chat state without asynchronous delays.
The open-source messaging application bitchat (permissionlesstech/bitchat) implements a privacy-first architecture featuring an instant data destruction mechanism and cryptographic identity rotation. Understanding how the triple-tap panic wipe coordinates service-level data erasure and how epoch-based peer-ID rotation prevents tracking requires examining the Swift implementation across the ViewModels and the cryptographic specifications governing the mesh network protocol.
How the Triple-Tap Panic Wipe Works
UI Detection and Trigger Mechanism
In bitchat/Views/ContentView.swift (around line 235), a triple-tap gesture recognizer attached to the microphone button initiates the emergency sequence. The UI layer immediately delegates to voiceRecordingVM?.panicWipe() and subsequently triggers the application-wide wipe through ChatViewModel.shared.panicClearAllData().
This two-stage dispatch ensures that time-critical audio capture terminates before the broader data erasure begins, preventing any in-flight recording from persisting to disk.
Aborting In-Flight Capture Sessions
The VoiceRecordingViewModel.swift file (lines 26-35) implements the first line of defense against data leakage during active recording. The panicWipe() method atomically increments holdGeneration (using &+= 1 for overflow safety) to invalidate any pending asynchronous callbacks, synchronously cancels the active capture session, and resets the UI state to idle.
// VoiceRecordingViewModel.swift
func panicWipe() {
holdGeneration &+= 1 // invalidate any pending callbacks
let session = activeSession
activeSession = nil
state = .idle
isLiveStreaming = false
session?.panicCancelSynchronously() // abort the microphone hardware
}
The generation guard pattern ensures that all capture-related APIs check generation == holdGeneration before proceeding, guaranteeing that a panic wipe instantly nullifies any in-flight operation regardless of thread timing.
Service-Wide Coordinated Erasure
The ChatViewModel (around line 1609) orchestrates a composite wipe across isolated services. Each service exposes its own synchronous panicWipe() method, making the overall operation composable and testable.
LocationStateManager (bitchat/Services/LocationStateManager.swift, lines 630-640) clears all persisted location state:
// LocationStateManager.swift
func panicWipe() {
storage.removeObject(forKey: selectedChannelKey)
storage.removeObject(forKey: teleportedStoreKey)
storage.removeObject(forKey: bookmarksKey)
storage.removeObject(forKey: bookmarkNamesKey)
teleportedSet.removeAll()
bookmarkMembership.removeAll()
bookmarks = []
bookmarkNames = [:]
teleported = false
selectedChannel = .mesh
}
BLEIncomingFileStore (bitchat/Services/BLE/BLEIncomingFileStore.swift, lines 196-340) deletes every managed media file, wipes receipt caches, and clears any pending file-transfer reservations. Additionally, the coordinator invalidates queued out-box callbacks, courier messages, and pending network activity.
The entire procedure is idempotent and synchronous; it does not rely on asynchronous work and instantly clears both in-memory state and persisted files. SwiftUI’s reactive bindings automatically refresh the UI when @Published properties (such as state, selectedChannel, and teleported) reset to their default values.
Identity Rotation and Peer-ID Management
The bitchat protocol implements epoch-based identity rotation to prevent passive observers from linking multiple observations of the same device. The specification in docs/PEER-ID-ROTATION.md defines the cryptographic primitives used to derive rotating 8-byte peer IDs while maintaining verifiable authenticity through signed binding proofs.
Epoch-Based Rotation Strategy
The current epoch calculates as floor(currentUnixTime / ROTATION_PERIOD), with a default rotation period of one hour. Devices accept peer IDs from epoch-1, epoch, and epoch+1 to tolerate clock skew across the mesh network. This deterministic approach ensures a device can recover the same ID after a restart while remaining unlinkable across different epochs.
Cryptographic Derivation of Rotating Peer IDs
The rotation derives from the static Noise private key using HKDF-SHA256:
- Rotation Secret:
K_rot = HKDF-SHA256(ikm: staticNoisePrivateKey, salt: "", info: "bitchat-peer-rotation-v1", length: 32) - Rotating Peer ID:
peerID_e = HMAC-SHA256(key: K_rot, message: "bitchat-peer-id-v2" || uint32be(epoch))[0..8]
The resulting 8-byte ID preserves packet header size while ensuring that only holders of the private static key can generate valid IDs for a given epoch.
Recognition Tags for Known Peers
For established relationships between peers A and B, the protocol derives a shared secret S_AB = X25519(A_priv, B_pub) and subsequently K_AB = HKDF-SHA256(..., info: "bitchat-recognition-v1"). Each direction maintains a distinct recognition tag:
tag_A→B = HMAC-SHA256(K_AB, epoch || A_pub || B_pub || peerID_e)[0..8]
These transient tags allow devices to recognize known contacts without revealing the underlying static identities to passive eavesdroppers.
Binding Proof in the Noise Handshake
To prevent ID forgery while rotating, the protocol employs a signed binding proof exchanged after the Noise XX handshake:
proof = Ed25519-Sign(signingPriv, "bitchat-peerid-binding-v1" || epoch || peerID_e || staticNoisePub)
Each side verifies this proof against the freshly established Noise static key, guaranteeing that the rotating ID belongs to the static key holder. This preserves the security guarantees of BLEAnnounceHandlingPolicy and NoiseSessionManager verification checks even as the visible peer ID changes hourly.
The rotation logic is currently gated behind the PeerCapabilities.peerIDRotation capability flag, with a planned transition to the announceV2 = 0x2C packet type.
Summary
- Triple-tap trigger: A gesture on the microphone button in
ContentView.swiftinitiatesVoiceRecordingViewModel.panicWipe(), which atomically invalidates callbacks and aborts hardware capture. - Synchronous erasure:
ChatViewModel.panicClearAllData()coordinatespanicWipe()calls acrossLocationStateManagerandBLEIncomingFileStore, clearing all persisted location data, bookmarks, and media files without async delays. - Generation guards: The
holdGenerationcounter ensures in-flight operations terminate before data deletion completes. - Epoch-based rotation: Peer IDs rotate deterministically each hour using HKDF-SHA256 and HMAC-SHA256 derived from the static Noise private key.
- Cryptographic binding: Ed25519-signed proofs within the Noise handshake bind rotating IDs to static keys, preventing forgery while maintaining unlinkability across epochs.
Frequently Asked Questions
What triggers the panic emergency wipe in bitchat?
A triple-tap gesture on the microphone button in ContentView.swift triggers the wipe. This UI action calls VoiceRecordingViewModel.panicWipe() to abort active recording, followed by ChatViewModel.panicClearAllData() to erase all persisted application data.
How does the generation guard prevent data leakage during a panic wipe?
The VoiceRecordingViewModel maintains a holdGeneration counter that increments atomically during panicWipe(). All asynchronous capture callbacks validate their generation token against this counter; mismatches cause immediate discard, ensuring that no audio data persists after the wipe initiates.
Why does bitchat rotate peer IDs every hour?
Hourly rotation (configurable via ROTATION_PERIOD) prevents passive network observers from correlating multiple sightings of the same device. By deriving new 8-byte peer IDs cryptographically from the static key and current epoch, the protocol ensures unlinkability across time periods while allowing deterministic recovery after app restarts.
How does the binding proof maintain security during identity rotation?
After the Noise XX handshake, each peer transmits an Ed25519-signed proof containing the current epoch, rotating peer ID, and static public key. This binds the ephemeral rotating ID to the long-term static key, ensuring that BLEAnnounceHandlingPolicy verification checks remain valid without exposing the static identity to passive observers.
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 →