mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-29 07:27:16 +00:00
fix: panic wipe erases persisted last-active conversation (#1064)
`panicClearAllData` wiped messages, keychain, identity defaults and location state, but never the new `conversation.lastActive` pointer, so after an emergency wipe the next launch could still restore the just-wiped DM or channel — a privacy-load-bearing gap for an activist-safety wipe. Adds `ConversationStore.clearPersistedLastActive()`, which removes the key through the store's own injected `storage` (never by reaching into `.standard`, and keeping `lastActiveKey` private), and calls it from the panic path right after `conversations.clearAll()`. Test: after a persisted restorable DM, a `clearPersistedLastActive()` makes the next launch fall back to the conversation list. NOT locally compiled (Swift/iOS; CI runs Build iOS + Swift Tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
87f1561f34
commit
8428dfed66
@ -417,6 +417,14 @@ final class ConversationStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Erases the persisted last-active conversation. Called from the panic
|
||||
/// wipe so a restored DM/channel pointer cannot survive an emergency clear.
|
||||
/// The store owns its injected `storage`, so the key is removed through it
|
||||
/// (never by reaching into `.standard`), and `lastActiveKey` stays private.
|
||||
func clearPersistedLastActive() {
|
||||
storage.removeObject(forKey: lastActiveKey)
|
||||
}
|
||||
|
||||
/// Decides what to present at launch from the value persisted last
|
||||
/// session. Pure aside from reading the init snapshot: performs no
|
||||
/// selection mutation and never writes `activeChannel`, so it cannot race
|
||||
|
||||
@ -1177,6 +1177,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
// single-writer ConversationStore; the derived `messages` view and
|
||||
// the legacy mirror empty with it)
|
||||
conversations.clearAll()
|
||||
// Also erase the persisted last-active pointer (#1064) so a wiped
|
||||
// DM/channel cannot be restored on the next launch.
|
||||
conversations.clearPersistedLastActive()
|
||||
pendingGeohashSystemMessages.removeAll()
|
||||
|
||||
// Delete all keychain data (including Noise and Nostr keys)
|
||||
|
||||
@ -87,6 +87,31 @@ final class ConversationStoreLastActiveTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func test_clearPersistedLastActiveErasesRestoreRecord() {
|
||||
// #1064 panic wipe: clearing the persisted last-active pointer means a
|
||||
// wiped DM/channel cannot be restored on the next launch.
|
||||
let storage = makeStorage()
|
||||
|
||||
// A DM is persisted and would otherwise restore.
|
||||
let session = ConversationStore(storage: storage)
|
||||
session.setSelectedPrivatePeer(peerID)
|
||||
XCTAssertEqual(
|
||||
ConversationStore(storage: storage)
|
||||
.restoreLastActiveConversation(isPeerResolvable: { _ in true }),
|
||||
.restoredDirectChat(peerID)
|
||||
)
|
||||
|
||||
// The panic path erases the pointer through the store's own storage.
|
||||
session.clearPersistedLastActive()
|
||||
|
||||
// The next launch has nothing to restore and falls back to the list.
|
||||
XCTAssertEqual(
|
||||
ConversationStore(storage: storage)
|
||||
.restoreLastActiveConversation(isPeerResolvable: { _ in true }),
|
||||
.conversationList
|
||||
)
|
||||
}
|
||||
|
||||
func test_firstLaunchPresentsConversationList() {
|
||||
let storage = makeStorage()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user