From 99a9fb2e1f30d8793a3f592a2638af6d3e52c4a4 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 10 Jan 2024 04:01:41 +0700 Subject: [PATCH] ios: self destruct improvements (#3640) * ios: self destruct improvements * test * adapted to stopped chat * wait until ctrl initialization finishes * Revert "test" This reverts commit 7c199293cc0193c7a0ee6e5d9977a4ed56b20098. * refactor * simplify,fix * refactor2 * refactor3 * comment * fix * fix * comment Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * flip and rename flag --------- Co-authored-by: Avently Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- apps/ios/Shared/ContentView.swift | 8 +++- apps/ios/Shared/Model/ChatModel.swift | 1 + apps/ios/Shared/Model/SimpleXAPI.swift | 2 + apps/ios/Shared/SimpleXApp.swift | 6 ++- .../Shared/Views/Database/DatabaseView.swift | 1 + .../Views/LocalAuth/LocalAuthView.swift | 42 +++++++++++++++---- .../Shared/Views/LocalAuth/PasscodeView.swift | 7 +++- .../Views/LocalAuth/SetAppPasscodeView.swift | 8 +++- .../Views/UserSettings/PrivacySettings.swift | 13 +++++- apps/ios/SimpleXChat/FileUtils.swift | 24 +++++++++-- 10 files changed, 91 insertions(+), 21 deletions(-) diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index d7b9fef21..c3a8ec280 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -31,6 +31,7 @@ struct ContentView: View { @State private var showWhatsNew = false @State private var showChooseLAMode = false @State private var showSetPasscode = false + @State private var waitingForOrPassedAuth = true @State private var chatListActionSheet: ChatListActionSheet? = nil private enum ChatListActionSheet: Identifiable { @@ -61,6 +62,10 @@ struct ContentView: View { } if !showSettings, let la = chatModel.laRequest { LocalAuthView(authRequest: la) + .onDisappear { + // this flag is separate from accessAuthenticated to show initializationView while we wait for authentication + waitingForOrPassedAuth = accessAuthenticated + } } else if showSetPasscode { SetAppPasscodeView { chatModel.contentViewAccessAuthenticated = true @@ -73,8 +78,7 @@ struct ContentView: View { showSetPasscode = false alertManager.showAlert(laPasscodeNotSetAlert()) } - } - if chatModel.chatDbStatus == nil { + } else if chatModel.chatDbStatus == nil && AppChatState.shared.value != .stopped && waitingForOrPassedAuth { initializationView() } } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index db0f13869..e5022751c 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -54,6 +54,7 @@ final class ChatModel: ObservableObject { @Published var chatDbChanged = false @Published var chatDbEncrypted: Bool? @Published var chatDbStatus: DBMigrationResult? + @Published var ctrlInitInProgress: Bool = false // local authentication @Published var contentViewAccessAuthenticated: Bool = false @Published var laRequest: LocalAuthRequest? diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 154328a7b..ee31dd65c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1215,6 +1215,8 @@ private func currentUserId(_ funcName: String) throws -> Int64 { func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws { logger.debug("initializeChat") let m = ChatModel.shared + m.ctrlInitInProgress = true + defer { m.ctrlInitInProgress = false } (m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations) if m.chatDbStatus != .ok { return } // If we migrated successfully means previous re-encryption process on database level finished successfully too diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 60d1cf725..e5b98589a 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -44,8 +44,10 @@ struct SimpleXApp: App { chatModel.appOpenUrl = url } .onAppear() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { - initChatAndMigrate() + if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + initChatAndMigrate() + } } } .onChange(of: scenePhase) { phase in diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 72515a1fa..31b1f618e 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -484,6 +484,7 @@ func deleteChatAsync() async throws { try await apiDeleteStorage() _ = kcDatabasePassword.remove() storeDBPassphraseGroupDefault.set(true) + deleteAppDatabaseAndFiles() } struct DatabaseView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift index bdb5b03e8..9691a9efd 100644 --- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift +++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift @@ -13,19 +13,28 @@ struct LocalAuthView: View { @EnvironmentObject var m: ChatModel var authRequest: LocalAuthRequest @State private var password = "" + @State private var allowToReact = true var body: some View { - PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") { + PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit", + buttonsEnabled: $allowToReact) { if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword { + allowToReact = false deleteStorageAndRestart(sdPassword) { r in m.laRequest = nil authRequest.completed(r) } return } - let r: LAResult = password == authRequest.password - ? .success - : .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry")) + let r: LAResult + if password == authRequest.password { + if authRequest.selfDestruct && kcSelfDestructPassword.get() != nil && !m.chatInitialized { + initChatAndMigrate() + } + r = .success + } else { + r = .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry")) + } m.laRequest = nil authRequest.completed(r) } cancel: { @@ -37,8 +46,27 @@ struct LocalAuthView: View { private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) { Task { do { - try await stopChatAsync() - try await deleteChatAsync() + /** Waiting until [initializeChat] finishes */ + while (m.ctrlInitInProgress) { + try await Task.sleep(nanoseconds: 50_000000) + } + if m.chatRunning == true { + try await stopChatAsync() + } + if m.chatInitialized { + /** + * The following sequence can bring a user here: + * the user opened the app, entered app passcode, went to background, returned back, entered self-destruct code. + * In this case database should be closed to prevent possible situation when OS can deny database removal command + * */ + chatCloseStore() + } + deleteAppDatabaseAndFiles() + // Clear sensitive data on screen just in case app fails to hide its views while new database is created + m.chatId = nil + m.reversedChatItems = [] + m.chats = [] + m.users = [] _ = kcAppPassword.set(password) _ = kcSelfDestructPassword.remove() await NtfManager.shared.removeAllNotifications() @@ -53,7 +81,7 @@ struct LocalAuthView: View { try initializeChat(start: true) m.chatDbChanged = false AppChatState.shared.set(.active) - if m.currentUser != nil { return } + if m.currentUser != nil || !m.chatInitialized { return } var profile: Profile? = nil if let displayName = displayName, displayName != "" { profile = Profile(displayName: displayName, fullName: "") diff --git a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift index c73ded2d2..9e0d7f38b 100644 --- a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift +++ b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift @@ -14,6 +14,8 @@ struct PasscodeView: View { var reason: String? = nil var submitLabel: LocalizedStringKey var submitEnabled: ((String) -> Bool)? + @Binding var buttonsEnabled: Bool + var submit: () -> Void var cancel: () -> Void @@ -70,11 +72,11 @@ struct PasscodeView: View { @ViewBuilder private func buttonsView() -> some View { Button(action: cancel) { Label("Cancel", systemImage: "multiply") - } + }.disabled(!buttonsEnabled) Button(action: submit) { Label(submitLabel, systemImage: "checkmark") } - .disabled(submitEnabled?(passcode) == false || passcode.count < 4) + .disabled(submitEnabled?(passcode) == false || passcode.count < 4 || !buttonsEnabled) } } @@ -85,6 +87,7 @@ struct PasscodeViewView_Previews: PreviewProvider { title: "Enter Passcode", reason: "Unlock app", submitLabel: "Submit", + buttonsEnabled: Binding.constant(true), submit: {}, cancel: {} ) diff --git a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift index 76cd3e279..7ec3ee1a4 100644 --- a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift +++ b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct SetAppPasscodeView: View { var passcodeKeychain: KeyChainItem = kcAppPassword + var prohibitedPasscodeKeychain: KeyChainItem = kcSelfDestructPassword var title: LocalizedStringKey = "New Passcode" var reason: String? var submit: () -> Void @@ -41,7 +42,10 @@ struct SetAppPasscodeView: View { } } } else { - setPasswordView(title: title, submitLabel: "Save") { + setPasswordView(title: title, + submitLabel: "Save", + // Do not allow to set app passcode == selfDestruct passcode + submitEnabled: { pwd in pwd != prohibitedPasscodeKeychain.get() }) { enteredPassword = passcode passcode = "" confirming = true @@ -54,7 +58,7 @@ struct SetAppPasscodeView: View { } private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View { - PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) { + PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, buttonsEnabled: Binding.constant(true), submit: submit) { dismiss() cancel() } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index d8ff2c2f8..8d13c6fb3 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -491,14 +491,23 @@ struct SimplexLockView: View { showLAAlert(.laPasscodeNotChangedAlert) } case .enableSelfDestruct: - SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) { + SetAppPasscodeView( + passcodeKeychain: kcSelfDestructPassword, + prohibitedPasscodeKeychain: kcAppPassword, + title: "Set passcode", + reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view") + ) { updateSelfDestruct() showLAAlert(.laSelfDestructPasscodeSetAlert) } cancel: { revertSelfDestruct() } case .changeSelfDestructPasscode: - SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) { + SetAppPasscodeView( + passcodeKeychain: kcSelfDestructPassword, + prohibitedPasscodeKeychain: kcAppPassword, + reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view") + ) { showLAAlert(.laSelfDestructPasscodeChangedAlert) } cancel: { showLAAlert(.laPasscodeNotChangedAlert) diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 60d281f14..748a7841d 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -69,13 +69,29 @@ func fileModificationDate(_ path: String) -> Date? { } } +public func deleteAppDatabaseAndFiles() { + let fm = FileManager.default + let dbPath = getAppDatabasePath().path + do { + try fm.removeItem(atPath: dbPath + CHAT_DB) + try fm.removeItem(atPath: dbPath + AGENT_DB) + } catch let error { + logger.error("Failed to delete all databases: \(error)") + } + try? fm.removeItem(atPath: dbPath + CHAT_DB_BAK) + try? fm.removeItem(atPath: dbPath + AGENT_DB_BAK) + try? fm.removeItem(at: getTempFilesDirectory()) + try? fm.createDirectory(at: getTempFilesDirectory(), withIntermediateDirectories: true) + deleteAppFiles() + _ = kcDatabasePassword.remove() + storeDBPassphraseGroupDefault.set(true) +} + public func deleteAppFiles() { let fm = FileManager.default do { - let fileNames = try fm.contentsOfDirectory(atPath: getAppFilesDirectory().path) - for fileName in fileNames { - removeFile(fileName) - } + try fm.removeItem(at: getAppFilesDirectory()) + try fm.createDirectory(at: getAppFilesDirectory(), withIntermediateDirectories: true) } catch { logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)") }