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:
parent
ce9d583b39
commit
99a9fb2e1f
@ -31,6 +31,7 @@ struct ContentView: View {
|
|||||||
@State private var showWhatsNew = false
|
@State private var showWhatsNew = false
|
||||||
@State private var showChooseLAMode = false
|
@State private var showChooseLAMode = false
|
||||||
@State private var showSetPasscode = false
|
@State private var showSetPasscode = false
|
||||||
|
@State private var waitingForOrPassedAuth = true
|
||||||
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
||||||
|
|
||||||
private enum ChatListActionSheet: Identifiable {
|
private enum ChatListActionSheet: Identifiable {
|
||||||
@ -61,6 +62,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
if !showSettings, let la = chatModel.laRequest {
|
if !showSettings, let la = chatModel.laRequest {
|
||||||
LocalAuthView(authRequest: la)
|
LocalAuthView(authRequest: la)
|
||||||
|
.onDisappear {
|
||||||
|
// this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
|
||||||
|
waitingForOrPassedAuth = accessAuthenticated
|
||||||
|
}
|
||||||
} else if showSetPasscode {
|
} else if showSetPasscode {
|
||||||
SetAppPasscodeView {
|
SetAppPasscodeView {
|
||||||
chatModel.contentViewAccessAuthenticated = true
|
chatModel.contentViewAccessAuthenticated = true
|
||||||
@ -73,8 +78,7 @@ struct ContentView: View {
|
|||||||
showSetPasscode = false
|
showSetPasscode = false
|
||||||
alertManager.showAlert(laPasscodeNotSetAlert())
|
alertManager.showAlert(laPasscodeNotSetAlert())
|
||||||
}
|
}
|
||||||
}
|
} else if chatModel.chatDbStatus == nil && AppChatState.shared.value != .stopped && waitingForOrPassedAuth {
|
||||||
if chatModel.chatDbStatus == nil {
|
|
||||||
initializationView()
|
initializationView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ final class ChatModel: ObservableObject {
|
|||||||
@Published var chatDbChanged = false
|
@Published var chatDbChanged = false
|
||||||
@Published var chatDbEncrypted: Bool?
|
@Published var chatDbEncrypted: Bool?
|
||||||
@Published var chatDbStatus: DBMigrationResult?
|
@Published var chatDbStatus: DBMigrationResult?
|
||||||
|
@Published var ctrlInitInProgress: Bool = false
|
||||||
// local authentication
|
// local authentication
|
||||||
@Published var contentViewAccessAuthenticated: Bool = false
|
@Published var contentViewAccessAuthenticated: Bool = false
|
||||||
@Published var laRequest: LocalAuthRequest?
|
@Published var laRequest: LocalAuthRequest?
|
||||||
|
@ -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 {
|
func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws {
|
||||||
logger.debug("initializeChat")
|
logger.debug("initializeChat")
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
|
m.ctrlInitInProgress = true
|
||||||
|
defer { m.ctrlInitInProgress = false }
|
||||||
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
|
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
|
||||||
if m.chatDbStatus != .ok { return }
|
if m.chatDbStatus != .ok { return }
|
||||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||||
|
@ -44,10 +44,12 @@ struct SimpleXApp: App {
|
|||||||
chatModel.appOpenUrl = url
|
chatModel.appOpenUrl = url
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
|
if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||||
initChatAndMigrate()
|
initChatAndMigrate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.onChange(of: scenePhase) { phase in
|
.onChange(of: scenePhase) { phase in
|
||||||
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
|
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
|
@ -484,6 +484,7 @@ func deleteChatAsync() async throws {
|
|||||||
try await apiDeleteStorage()
|
try await apiDeleteStorage()
|
||||||
_ = kcDatabasePassword.remove()
|
_ = kcDatabasePassword.remove()
|
||||||
storeDBPassphraseGroupDefault.set(true)
|
storeDBPassphraseGroupDefault.set(true)
|
||||||
|
deleteAppDatabaseAndFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DatabaseView_Previews: PreviewProvider {
|
struct DatabaseView_Previews: PreviewProvider {
|
||||||
|
@ -13,19 +13,28 @@ struct LocalAuthView: View {
|
|||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
var authRequest: LocalAuthRequest
|
var authRequest: LocalAuthRequest
|
||||||
@State private var password = ""
|
@State private var password = ""
|
||||||
|
@State private var allowToReact = true
|
||||||
|
|
||||||
var body: some View {
|
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 {
|
if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword {
|
||||||
|
allowToReact = false
|
||||||
deleteStorageAndRestart(sdPassword) { r in
|
deleteStorageAndRestart(sdPassword) { r in
|
||||||
m.laRequest = nil
|
m.laRequest = nil
|
||||||
authRequest.completed(r)
|
authRequest.completed(r)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let r: LAResult = password == authRequest.password
|
let r: LAResult
|
||||||
? .success
|
if password == authRequest.password {
|
||||||
: .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
|
if authRequest.selfDestruct && kcSelfDestructPassword.get() != nil && !m.chatInitialized {
|
||||||
|
initChatAndMigrate()
|
||||||
|
}
|
||||||
|
r = .success
|
||||||
|
} else {
|
||||||
|
r = .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
|
||||||
|
}
|
||||||
m.laRequest = nil
|
m.laRequest = nil
|
||||||
authRequest.completed(r)
|
authRequest.completed(r)
|
||||||
} cancel: {
|
} cancel: {
|
||||||
@ -37,8 +46,27 @@ struct LocalAuthView: View {
|
|||||||
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
|
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
|
/** Waiting until [initializeChat] finishes */
|
||||||
|
while (m.ctrlInitInProgress) {
|
||||||
|
try await Task.sleep(nanoseconds: 50_000000)
|
||||||
|
}
|
||||||
|
if m.chatRunning == true {
|
||||||
try await stopChatAsync()
|
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)
|
_ = kcAppPassword.set(password)
|
||||||
_ = kcSelfDestructPassword.remove()
|
_ = kcSelfDestructPassword.remove()
|
||||||
await NtfManager.shared.removeAllNotifications()
|
await NtfManager.shared.removeAllNotifications()
|
||||||
@ -53,7 +81,7 @@ struct LocalAuthView: View {
|
|||||||
try initializeChat(start: true)
|
try initializeChat(start: true)
|
||||||
m.chatDbChanged = false
|
m.chatDbChanged = false
|
||||||
AppChatState.shared.set(.active)
|
AppChatState.shared.set(.active)
|
||||||
if m.currentUser != nil { return }
|
if m.currentUser != nil || !m.chatInitialized { return }
|
||||||
var profile: Profile? = nil
|
var profile: Profile? = nil
|
||||||
if let displayName = displayName, displayName != "" {
|
if let displayName = displayName, displayName != "" {
|
||||||
profile = Profile(displayName: displayName, fullName: "")
|
profile = Profile(displayName: displayName, fullName: "")
|
||||||
|
@ -14,6 +14,8 @@ struct PasscodeView: View {
|
|||||||
var reason: String? = nil
|
var reason: String? = nil
|
||||||
var submitLabel: LocalizedStringKey
|
var submitLabel: LocalizedStringKey
|
||||||
var submitEnabled: ((String) -> Bool)?
|
var submitEnabled: ((String) -> Bool)?
|
||||||
|
@Binding var buttonsEnabled: Bool
|
||||||
|
|
||||||
var submit: () -> Void
|
var submit: () -> Void
|
||||||
var cancel: () -> Void
|
var cancel: () -> Void
|
||||||
|
|
||||||
@ -70,11 +72,11 @@ struct PasscodeView: View {
|
|||||||
@ViewBuilder private func buttonsView() -> some View {
|
@ViewBuilder private func buttonsView() -> some View {
|
||||||
Button(action: cancel) {
|
Button(action: cancel) {
|
||||||
Label("Cancel", systemImage: "multiply")
|
Label("Cancel", systemImage: "multiply")
|
||||||
}
|
}.disabled(!buttonsEnabled)
|
||||||
Button(action: submit) {
|
Button(action: submit) {
|
||||||
Label(submitLabel, systemImage: "checkmark")
|
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",
|
title: "Enter Passcode",
|
||||||
reason: "Unlock app",
|
reason: "Unlock app",
|
||||||
submitLabel: "Submit",
|
submitLabel: "Submit",
|
||||||
|
buttonsEnabled: Binding.constant(true),
|
||||||
submit: {},
|
submit: {},
|
||||||
cancel: {}
|
cancel: {}
|
||||||
)
|
)
|
||||||
|
@ -11,6 +11,7 @@ import SimpleXChat
|
|||||||
|
|
||||||
struct SetAppPasscodeView: View {
|
struct SetAppPasscodeView: View {
|
||||||
var passcodeKeychain: KeyChainItem = kcAppPassword
|
var passcodeKeychain: KeyChainItem = kcAppPassword
|
||||||
|
var prohibitedPasscodeKeychain: KeyChainItem = kcSelfDestructPassword
|
||||||
var title: LocalizedStringKey = "New Passcode"
|
var title: LocalizedStringKey = "New Passcode"
|
||||||
var reason: String?
|
var reason: String?
|
||||||
var submit: () -> Void
|
var submit: () -> Void
|
||||||
@ -41,7 +42,10 @@ struct SetAppPasscodeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
enteredPassword = passcode
|
||||||
passcode = ""
|
passcode = ""
|
||||||
confirming = true
|
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 {
|
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()
|
dismiss()
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
@ -491,14 +491,23 @@ struct SimplexLockView: View {
|
|||||||
showLAAlert(.laPasscodeNotChangedAlert)
|
showLAAlert(.laPasscodeNotChangedAlert)
|
||||||
}
|
}
|
||||||
case .enableSelfDestruct:
|
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()
|
updateSelfDestruct()
|
||||||
showLAAlert(.laSelfDestructPasscodeSetAlert)
|
showLAAlert(.laSelfDestructPasscodeSetAlert)
|
||||||
} cancel: {
|
} cancel: {
|
||||||
revertSelfDestruct()
|
revertSelfDestruct()
|
||||||
}
|
}
|
||||||
case .changeSelfDestructPasscode:
|
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)
|
showLAAlert(.laSelfDestructPasscodeChangedAlert)
|
||||||
} cancel: {
|
} cancel: {
|
||||||
showLAAlert(.laPasscodeNotChangedAlert)
|
showLAAlert(.laPasscodeNotChangedAlert)
|
||||||
|
@ -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() {
|
public func deleteAppFiles() {
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
do {
|
do {
|
||||||
let fileNames = try fm.contentsOfDirectory(atPath: getAppFilesDirectory().path)
|
try fm.removeItem(at: getAppFilesDirectory())
|
||||||
for fileName in fileNames {
|
try fm.createDirectory(at: getAppFilesDirectory(), withIntermediateDirectories: true)
|
||||||
removeFile(fileName)
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)")
|
logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user