ios: self destruct improvements (#3640)

* ios: self destruct improvements

* test

* adapted to stopped chat

* wait until ctrl initialization finishes

* Revert "test"

This reverts commit 7c199293cc.

* 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 <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko 2024-01-10 04:01:41 +07:00 committed by GitHub
parent ce9d583b39
commit 99a9fb2e1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 91 additions and 21 deletions

View File

@ -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()
}
}

View File

@ -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?

View File

@ -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

View File

@ -44,10 +44,12 @@ struct SimpleXApp: App {
chatModel.appOpenUrl = url
}
.onAppear() {
if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
initChatAndMigrate()
}
}
}
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
switch (phase) {

View File

@ -484,6 +484,7 @@ func deleteChatAsync() async throws {
try await apiDeleteStorage()
_ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true)
deleteAppDatabaseAndFiles()
}
struct DatabaseView_Previews: PreviewProvider {

View File

@ -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 {
/** Waiting until [initializeChat] finishes */
while (m.ctrlInitInProgress) {
try await Task.sleep(nanoseconds: 50_000000)
}
if m.chatRunning == true {
try await stopChatAsync()
try await deleteChatAsync()
}
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: "")

View File

@ -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: {}
)

View File

@ -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()
}

View File

@ -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)

View File

@ -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)")
}