# How the Panic Emergency Wipe (Triple-Tap) and Identity Rotation Work in Bitchat

> Discover how Bitchat's panic emergency wipe (triple-tap) instantly erases sensitive data and manages identity rotation. Learn about secure data deletion and generation counters.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-09

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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.

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/LocationStateManager.swift), lines 630-640) clears all persisted location state:

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ContentView.swift) initiates `VoiceRecordingViewModel.panicWipe()`, which atomically invalidates callbacks and aborts hardware capture.
- **Synchronous erasure**: `ChatViewModel.panicClearAllData()` coordinates `panicWipe()` calls across `LocationStateManager` and `BLEIncomingFileStore`, clearing all persisted location data, bookmarks, and media files without async delays.
- **Generation guards**: The `holdGeneration` counter 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`](https://github.com/permissionlesstech/bitchat/blob/main/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.