BitChat Emergency Wipe Implementation: How the Panic Wipe Feature Destroys Data Securely

BitChat's emergency wipe is a coordinated, multi-layered transaction that atomically erases all locally stored data—including messages, media, cryptographic keys, and even app-switcher snapshots—while using durable recovery markers to guarantee complete destruction even if the app crashes mid-operation.

The panic wipe in BitChat (permissionlesstech/bitchat) serves as a last-resort privacy mechanism for users facing device compromise or coercion. This feature goes far beyond simple file deletion, implementing defense-in-depth strategies that thwart forensic recovery attempts and ensure no residual identity data survives.

Architecture of the Panic Wipe System

The emergency wipe operates through eleven coordinated layers, each targeting a specific attack surface for data persistence.

Layer Responsibility Key Implementation
View-Model entry point Orchestrates the entire wipe operation ChatViewModel.panicClearAllData()
Service shutdown Prevents network callbacks from resurrecting cleared state panicNetworkLifecycle.stop(), meshService.emergencyDisconnectAll()
Durable intent creation Survives crashes via redundant markers in UserDefaults and filesystem PanicRecoveryOperations.live()
Media wipe Destroys voice notes, images, and files with directory recreation BLEIncomingFileStore.panicWipe()
Keychain wipe Eliminates all cryptographic secrets atomically KeychainManager.deleteAllKeychainData()
UserDefaults & identity Clears identity keys and randomizes nickname userDefaults.removeObject(), nickname = "anon…"
Location & presence Removes geohash state and bookmarks that reveal physical location LocationStateManager.shared.panicWipe()
Store clearing Purges conversation, courier, board, groups, and pre-key stores conversations.clearAll(), CourierStore.shared.wipe()
Nostr & relay state Resets relay identity and pending decrypts NostrRelayManager.shared.resetForPanicWipe()
BLE identity reset Suspends radio, wipes BLE identity, restarts under new identity panicTransport.resetIdentityForPanic()
App-switcher snapshot purge Deletes iOS UI screenshots in ~/Library/Caches/Snapshots/ clearAppSwitcherSnapshots()

The atomicity guarantee stems from generation counter validation: any background task holding pre-wipe keys encounters a mismatch after the wipe and aborts immediately, preventing data resurrection.

Trigger Mechanisms

BitChat provides two activation paths for the emergency wipe feature:

  • Settings button: A destructive-role button labeled via localized strings in AppInfoView.swift
  • Hidden gesture: Triple-tap on the BitChat logo for rapid activation under duress

Both paths converge on ChatViewModel.panicClearAllData() at lines 1553-1755 of ChatViewModel.swift.

Step-by-Step Execution Flow

The panic wipe proceeds through ten deterministic phases:

  1. User initiation → ChatViewModel.panicClearAllData() receives control
  2. Network isolation → panicNetworkLifecycle.stop() halts all connectivity; BLE transport suspends
  3. Crash recovery preparation → PanicRecoveryOperations.live().begin() writes to UserDefaults and creates .panic-recovery-pending marker file
  4. Media destruction → BLEIncomingFileStore.panicWipe() deletes and recreates media directories
  5. Cryptographic purge → KeychainManager.deleteAllKeychainData() enumerates and wipes all keychain entries
  6. Store eradication → Each persistent store executes wipe() for on-disk files and associated secrets
  7. Identity reset → Noise identity keys cleared; random anonymous nickname assigned
  8. Transport reset → Nostr relay manager and BLE transport reset identities and queues
  9. Snapshot elimination → clearAppSwitcherSnapshots() removes iOS app switcher thumbnails
  10. Recovery commitment → panicRecoveryOperations.complete() removes durable markers; services restart on success

Failure at any step preserves the recovery intent, triggering automatic retry on next app launch.

Core Implementation: panicClearAllData()

The central coordinator method in ChatViewModel.swift implements defensive execution patterns:

@discardableResult
func panicClearAllData(restartServices: Bool = true) -> Bool {
    panicRecoveryBlocked = true
    isPanicResetting = true
    defer { isPanicResetting = false }

    // 1️⃣ Stop services
    panicNetworkLifecycle.stop()
    if let panicTransport = meshService as? PanicResettingTransport {
        panicTransport.suspendForPanicReset()
    } else {
        meshService.emergencyDisconnectAll()
    }

    // 2️⃣ Create durable intent
    let recoveryIntent = panicRecoveryOperations.begin()

    // 3️⃣ Media & store wipes
    mediaTransferCoordinator.resetForPanic()
    liveVoiceCoordinator.resetForPanic()
    conversations.clearAll()
    keychain.deleteAllKeychainData()
    LocationStateManager.shared.panicWipe()
    // … additional store wipes

    // 4️⃣ Finalise recovery
    do {
        try panicRecoveryOperations.wipeMedia(recoveryIntent)
        try panicRecoveryOperations.complete()
        panicRecoveryBlocked = false
        return true
    } catch {
        // Recovery will retry on next launch
        return false
    }
}

Key defensive patterns include:

  • panicRecoveryBlocked flag: Prevents re-entrant wipe attempts
  • defer with isPanicResetting: Ensures state cleanup regardless of exit path
  • Typed throws: Distinguishes recoverable from fatal errors

Media File Destruction: BLEIncomingFileStore.panicWipe()

Media files receive special handling due to their detached task exposure:

func panicWipe(hasDurablePendingMarker: Bool = false) throws {
    // Delete every sub-directory of managed media
    for sub in Self.mediaSubdirectories {
        let dir = baseDirectory?.appendingPathComponent(sub)
        try fileManager.removeItem(at: dir)
        try fileManager.createDirectory(at: dir, withIntermediateDirectories: true,
                                        attributes: Self.mediaProtectionAttributes)
    }
    // Optional: write marker file for crash recovery coordination
    if hasDurablePendingMarker {
        let markerURL = baseDirectory?.appendingPathComponent(Self.panicRecoveryPendingMarkerFileName)
        try panicMarkerWriter(Data(), markerURL!)
    }
}

The directory recreation step eliminates race conditions where detached tasks might recreate files in stale paths.

Cryptographic Key Elimination

The keychain wipe in KeychainManager.swift (lines 668-752) uses enumeration rather than hardcoded key lists:

func deleteAllKeychainData() -> Bool {
    SecureLogger.warning("Panic mode - deleting all keychain data", category: .security)
    let searchQuery: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecMatchLimit as String: kSecMatchLimitAll,
        kSecReturnAttributes as String: true
    ]
    var result: AnyObject?
    let status = SecItemCopyMatching(searchQuery as CFDictionary, &result)
    // Enumerate and delete each owned item …
}

This approach catches legacy and dynamically created entries that explicit key lists would miss.

Crash Recovery Guarantees

The PanicRecoveryOperations system implements two-phase commit semantics:

  • live() mode: Persists intent to both UserDefaults and filesystem
  • begin(): Creates markers before any destructive operation
  • complete(): Removes markers only after all wipes succeed

If the process terminates between begin() and complete(), the next launch detects pending markers via live() and re-executes the full wipe sequence.

Source Files and Locations

File Function Lines
ChatViewModel.swift panicClearAllData() 1553-1755
ChatViewModel.swift clearAppSwitcherSnapshots() 81-87
BLEIncomingFileStore.swift panicWipe() 89-100
KeychainManager.swift deleteAllKeychainData() 668-752
LocationStateManager.swift panicWipe() 628-640
PanicRecoveryOperations.swift live(), begin(), complete() Full file
AppInfoView.swift UI button configuration 122-127

Summary

  • BitChat's panic wipe is a transactionally consistent, crash-recoverable operation that destroys all local data across eleven independent layers
  • Durable intent markers in both UserDefaults and filesystem guarantee completion even if the app terminates unexpectedly
  • Service shutdown precedes data destruction to prevent network callbacks from re-establishing state
  • Generation counter validation provides implicit atomicity by invalidating all pre-wipe cryptographic contexts
  • Directory recreation and snapshot purging address forensic recovery vectors often ignored by simpler wipe implementations

Frequently Asked Questions

What triggers the BitChat emergency wipe feature?

The panic wipe activates through either a destructive-role button in Settings (defined in AppInfoView.swift) or a hidden triple-tap gesture on the BitChat logo. Both paths invoke ChatViewModel.panicClearAllData() after user confirmation.

Does the BitChat panic wipe work if the app crashes mid-operation?

Yes. The PanicRecoveryOperations.live() system writes durable markers to both UserDefaults and a filesystem file (.panic-recovery-pending) before any destructive operations. If the app crashes, these markers persist and trigger automatic wipe completion on the next launch.

What data does the BitChat emergency wipe actually destroy?

The wipe eliminates messages, conversation history, media files, cryptographic keys from the iOS Keychain, geohash location data, Nostr relay state, BLE identity, and iOS app-switcher screenshots. It also randomizes the user's nickname to sever social graph associations.

How does BitChat prevent data recovery after a panic wipe?

Multiple mechanisms prevent recovery: keychain enumeration catches all secrets including legacy entries; media directories are deleted and immediately recreated to foil undeletion; app-switcher screenshots are purged from ~/Library/Caches/Snapshots/; and cryptographic generation counters invalidate any cached keys held by background tasks.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →