From 06835ee3fc1cd6243da1dca206906ad208e9fe54 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 8 Sep 2022 17:36:16 +0100 Subject: [PATCH] ios: additional db encryption UX (#1031) * ios: additional db encryption UX * typo Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * fixes Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- .../Database/DatabaseEncryptionView.swift | 10 +-- .../Views/Database/DatabaseErrorView.swift | 68 ++++++++++++++----- .../Shared/Views/Database/DatabaseView.swift | 21 +++++- apps/ios/Shared/Views/TerminalView.swift | 20 ++++++ src/Simplex/Chat.hs | 2 +- tests/ChatTests.hs | 4 +- 6 files changed, 98 insertions(+), 27 deletions(-) diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift index 374133355b..7358d9a45b 100644 --- a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -35,9 +35,9 @@ enum DatabaseEncryptionAlert: Identifiable { struct DatabaseEncryptionView: View { @EnvironmentObject private var m: ChatModel + @Binding var useKeychain: Bool @State private var alert: DatabaseEncryptionAlert? = nil @State private var progressIndicator = false - @State private var useKeychain = storeDBPassphraseGroupDefault.get() @State private var useKeychainToggle = storeDBPassphraseGroupDefault.get() @State private var initialRandomDBPassphrase = initialRandomDBPassphraseGroupDefault.get() @State private var storedKey = getDatabaseKey() != nil @@ -58,7 +58,7 @@ struct DatabaseEncryptionView: View { private func databaseEncryptionView() -> some View { List { Section { - settingsRow("key") { + settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) { Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle) .onChange(of: useKeychainToggle) { _ in if useKeychainToggle { @@ -224,7 +224,7 @@ struct DatabaseEncryptionView: View { case .currentPassphraseError: return Alert( title: Text("Wrong passsphrase!"), - message: Text("Please enter correct current passphrase") + message: Text("Please enter correct current passphrase.") ) case let .error(title, error): return Alert(title: Text(title), message: Text("\(error)")) @@ -254,6 +254,7 @@ struct DatabaseKeyField: View { var placeholder: LocalizedStringKey var valid: Bool var showStrength = false + var onSubmit: () -> Void = {} @State private var showKey = false var body: some View { @@ -272,6 +273,7 @@ struct DatabaseKeyField: View { .autocapitalization(.none) .submitLabel(.done) .padding(.leading, 36) + .onSubmit(onSubmit) } } @@ -338,6 +340,6 @@ func validKey(_ s: String) -> Bool { struct DatabaseEncryptionView_Previews: PreviewProvider { static var previews: some View { - DatabaseEncryptionView() + DatabaseEncryptionView(useKeychain: Binding.constant(true)) } } diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index 310c0d96ea..9b3c8c0fb0 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -11,7 +11,7 @@ import SimpleXChat struct DatabaseErrorView: View { @EnvironmentObject var m: ChatModel - var status: DBMigrationResult + @State var status: DBMigrationResult @State private var dbKey = "" @State private var storedDBKey = getDatabaseKey() @State private var useKeychain = storeDBPassphraseGroupDefault.get() @@ -23,17 +23,18 @@ struct DatabaseErrorView: View { if useKeychain && storedDBKey != nil && storedDBKey != "" { Text("Wrong database passphrase").font(.title) Text("Database passphrase is different from saved in the keychain.") - DatabaseKeyField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey)) + databaseKeyField(onSubmit: saveAndRunChat) saveAndOpenButton() Spacer() Text("File: \(dbFile)") } else { Text("Encrypted database").font(.title) Text("Database passphrase is required to open chat.") - DatabaseKeyField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey)) if useKeychain { + databaseKeyField(onSubmit: saveAndRunChat) saveAndOpenButton() } else { + databaseKeyField(onSubmit: runChat) openChatButton() } Spacer() @@ -59,29 +60,62 @@ struct DatabaseErrorView: View { } } .padding() - .frame(maxHeight: .infinity) } + .frame(maxHeight: .infinity) + } + + private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View { + DatabaseKeyField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit) + } private func saveAndOpenButton() -> some View { Button("Save passphrase and open chat") { - if setDatabaseKey(dbKey) { - storeDBPassphraseGroupDefault.set(true) - initialRandomDBPassphraseGroupDefault.set(false) - } - do { - try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey) - } catch let error { - logger.error("initializeChat \(responseError(error))") - } + saveAndRunChat() } } private func openChatButton() -> some View { Button("Open chat") { - do { - try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey) - } catch let error { - logger.error("initializeChat \(responseError(error))") + runChat() + } + } + + private func saveAndRunChat() { + if setDatabaseKey(dbKey) { + storeDBPassphraseGroupDefault.set(true) + initialRandomDBPassphraseGroupDefault.set(false) + } + runChat() + } + + private func runChat() { + do { + try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey) + if let s = m.chatDbStatus { + status = s + let am = AlertManager.shared + switch s { + case .errorNotADatabase: + am.showAlertMsg( + title: "Wrong passphrase!", + message: "Enter correct passphrase." + ) + case .errorKeychain: + am.showAlertMsg(title: "Keychain error") + case let .error(_, error): + am.showAlert(Alert( + title: Text("Database error"), + message: Text(error) + )) + case let .unknown(error): + am.showAlert(Alert( + title: Text("Unknown error"), + message: Text(error) + )) + case .ok: () + } } + } catch let error { + logger.error("initializeChat \(responseError(error))") } } } diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 8f7ba7cca4..5fe3b7b836 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -45,6 +45,7 @@ struct DatabaseView: View { @AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0 @State private var dbContainer = dbContainerGroupDefault.get() @State private var legacyDatabase = hasLegacyDatabase() + @State private var useKeychain = storeDBPassphraseGroupDefault.get() var body: some View { ZStack { @@ -86,9 +87,9 @@ struct DatabaseView: View { Section { let unencrypted = m.chatDbEncrypted == false let color: Color = unencrypted ? .orange : .secondary - settingsRow(unencrypted ? "lock.open" : "lock", color: color) { + settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) { NavigationLink { - DatabaseEncryptionView() + DatabaseEncryptionView(useKeychain: $useKeychain) .navigationTitle("Database passphrase") } label: { Text("Database passphrase") @@ -168,7 +169,7 @@ struct DatabaseView: View { title: Text("Stop chat?"), message: Text("Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped."), primaryButton: .destructive(Text("Stop")) { - stopChat() + authStopChat() }, secondaryButton: .cancel { withAnimation { runChat = true } @@ -226,6 +227,20 @@ struct DatabaseView: View { } } + private func authStopChat() { + if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) { + authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in + switch laResult { + case .success: stopChat() + case .unavailable: stopChat() + case .failed: withAnimation { runChat = true } + } + } + } else { + stopChat() + } + } + private func stopChat() { Task { do { diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 6aa9e8804c..488602dda6 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -17,8 +17,28 @@ struct TerminalView: View { @EnvironmentObject var chatModel: ChatModel @State var composeState: ComposeState = ComposeState() @FocusState private var keyboardVisible: Bool + @State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) var body: some View { + if authorized { + terminalView() + } else { + Button(action: runAuth) { Label("Unlock", systemImage: "lock") } + .onAppear(perform: runAuth) + } + } + + private func runAuth() { + authenticate(reason: NSLocalizedString("Open chat console", comment: "authentication reason")) { laResult in + switch laResult { + case .success: authorized = true + case .unavailable: authorized = true + case .failed: authorized = false + } + } + } + + private func terminalView() -> some View { VStack { ScrollViewReader { proxy in ScrollView { diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 70d0272c55..65670eb262 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2546,7 +2546,7 @@ chatCommandP = "/_db delete" $> APIDeleteStorage, "/_db encryption " *> (APIStorageEncryption <$> jsonP), "/db encrypt " *> (APIStorageEncryption . DBEncryptionConfig "" <$> dbKeyP), - "/db password " *> (APIStorageEncryption <$> (DBEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)), + "/db key " *> (APIStorageEncryption <$> (DBEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)), "/db decrypt " *> (APIStorageEncryption . (`DBEncryptionConfig` "") <$> dbKeyP), "/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)), "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional searchP), diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 983258fd44..1325db5620 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -2788,9 +2788,9 @@ testDatabaseEncryption = withTmpFiles $ do testChatWorking alice bob alice ##> "/_stop" alice <## "chat stopped" - alice ##> "/db password wrongkey nextkey" + alice ##> "/db key wrongkey nextkey" alice <## "error encrypting database: wrong passphrase or invalid database file" - alice ##> "/db password mykey nextkey" + alice ##> "/db key mykey nextkey" alice <## "ok" alice ##> "/_db encryption {\"currentKey\":\"nextkey\",\"newKey\":\"anotherkey\"}" alice <## "ok"