mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 16:45:22 +00:00
Make panic wipe deterministic and device-bound (#1431)
* Make panic wipe deterministic and device-bound * Scope install markers to iOS * Harden panic recovery and service shutdown * Invalidate queued BLE ingress during panic * Harden panic keychain and media cleanup --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: jack <jack@deck.local>
This commit is contained in:
@@ -370,6 +370,132 @@ struct BLEFileTransferHandlerTests {
|
||||
#expect(!FileManager.default.fileExists(atPath: evictable.path))
|
||||
}
|
||||
|
||||
@Test
|
||||
func panicWipeDeletesEveryManagedMediaFileAndRecreatesEmptyDirectories() throws {
|
||||
let base = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("panic-media-wipe-\(UUID().uuidString)", isDirectory: true)
|
||||
defer { try? FileManager.default.removeItem(at: base) }
|
||||
let store = BLEIncomingFileStore(baseDirectory: base)
|
||||
let subdirectories = [
|
||||
"voicenotes/incoming",
|
||||
"voicenotes/outgoing",
|
||||
"images/incoming",
|
||||
"images/outgoing",
|
||||
"files/incoming",
|
||||
"files/outgoing"
|
||||
]
|
||||
|
||||
for subdirectory in subdirectories {
|
||||
let directory = base
|
||||
.appendingPathComponent("files", isDirectory: true)
|
||||
.appendingPathComponent(subdirectory, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
try Data("secret".utf8).write(to: directory.appendingPathComponent("artifact.bin"))
|
||||
}
|
||||
let unmanaged = base.appendingPathComponent("files/legacy/secret.bin")
|
||||
try FileManager.default.createDirectory(at: unmanaged.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
try Data("legacy".utf8).write(to: unmanaged)
|
||||
|
||||
try store.panicWipe()
|
||||
|
||||
#expect(!FileManager.default.fileExists(atPath: unmanaged.path))
|
||||
for subdirectory in subdirectories {
|
||||
let directory = base
|
||||
.appendingPathComponent("files", isDirectory: true)
|
||||
.appendingPathComponent(subdirectory, isDirectory: true)
|
||||
var isDirectory: ObjCBool = false
|
||||
#expect(FileManager.default.fileExists(atPath: directory.path, isDirectory: &isDirectory))
|
||||
#expect(isDirectory.boolValue)
|
||||
#expect(try FileManager.default.contentsOfDirectory(atPath: directory.path).isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func panicWipeAttemptsDeletionWhenMarkerPersistenceFails() throws {
|
||||
enum MarkerFailure: Error { case unavailable }
|
||||
|
||||
let base = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(
|
||||
"panic-marker-failure-\(UUID().uuidString)",
|
||||
isDirectory: true
|
||||
)
|
||||
defer { try? FileManager.default.removeItem(at: base) }
|
||||
let secret = base
|
||||
.appendingPathComponent("files/images/outgoing", isDirectory: true)
|
||||
.appendingPathComponent("secret.jpg")
|
||||
try FileManager.default.createDirectory(
|
||||
at: secret.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
try Data("secret".utf8).write(to: secret)
|
||||
let store = BLEIncomingFileStore(
|
||||
baseDirectory: base,
|
||||
panicMarkerWriter: { _, _ in throw MarkerFailure.unavailable }
|
||||
)
|
||||
|
||||
do {
|
||||
try store.panicWipe(hasDurablePendingMarker: false)
|
||||
Issue.record("Expected the missing durable marker to fail closed")
|
||||
} catch {
|
||||
// The marker error is reported only after the deletion attempt.
|
||||
}
|
||||
|
||||
#expect(!FileManager.default.fileExists(atPath: secret.path))
|
||||
#expect(
|
||||
FileManager.default.fileExists(
|
||||
atPath: secret.deletingLastPathComponent().path
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func externalMarkerAllowsDeletionToCommitWhenFileMarkerFails() throws {
|
||||
enum MarkerFailure: Error { case unavailable }
|
||||
|
||||
let base = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(
|
||||
"panic-external-marker-\(UUID().uuidString)",
|
||||
isDirectory: true
|
||||
)
|
||||
defer { try? FileManager.default.removeItem(at: base) }
|
||||
let secret = base
|
||||
.appendingPathComponent("files/voicenotes/incoming", isDirectory: true)
|
||||
.appendingPathComponent("secret.m4a")
|
||||
try FileManager.default.createDirectory(
|
||||
at: secret.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
try Data("secret".utf8).write(to: secret)
|
||||
let store = BLEIncomingFileStore(
|
||||
baseDirectory: base,
|
||||
panicMarkerWriter: { _, _ in throw MarkerFailure.unavailable }
|
||||
)
|
||||
|
||||
try store.panicWipe(hasDurablePendingMarker: true)
|
||||
|
||||
#expect(!FileManager.default.fileExists(atPath: secret.path))
|
||||
}
|
||||
|
||||
@Test
|
||||
func panicRecoveryMarkerPersistsUntilExplicitCommit() throws {
|
||||
let base = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(
|
||||
"panic-recovery-marker-\(UUID().uuidString)",
|
||||
isDirectory: true
|
||||
)
|
||||
defer { try? FileManager.default.removeItem(at: base) }
|
||||
let store = BLEIncomingFileStore(baseDirectory: base)
|
||||
|
||||
try store.markPanicRecoveryPending()
|
||||
#expect(try store.isPanicRecoveryPending())
|
||||
try store.panicWipe(hasDurablePendingMarker: true)
|
||||
#expect(try store.isPanicRecoveryPending())
|
||||
|
||||
try store.completePanicRecovery()
|
||||
|
||||
#expect(try !store.isPanicRecoveryPending())
|
||||
}
|
||||
|
||||
private func expectNoSideEffects(_ recorder: Recorder) {
|
||||
#expect(recorder.signedNameQueries.isEmpty)
|
||||
#expect(recorder.trackedPackets.isEmpty)
|
||||
|
||||
Reference in New Issue
Block a user