diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 272a327e71..9c2ee6827b 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SimpleXChat struct ContentView: View { @EnvironmentObject var chatModel: ChatModel @@ -21,6 +22,8 @@ struct ContentView: View { ZStack { if prefPerformLA && userAuthorized != true { Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") } + } else if let status = chatModel.chatDbStatus, status != .ok { + DatabaseErrorView(status: status) } else if !chatModel.v3DBMigration.startChat { MigrateToAppGroupView() } else if let step = chatModel.onboardingStage { diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index ab1739846a..d349294a0f 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -18,6 +18,8 @@ final class ChatModel: ObservableObject { @Published var currentUser: User? @Published var chatRunning: Bool? @Published var chatDbChanged = false + @Published var chatDbEncrypted: Bool? + @Published var chatDbStatus: DBMigrationResult? // list of chat "previews" @Published var chats: [Chat] = [] // current chat diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 6db4b01075..f5a866b2b4 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -94,7 +94,7 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") } DispatchQueue.main.async { - ChatModel.shared.terminalItems.append(.cmd(.now, cmd)) + ChatModel.shared.terminalItems.append(.cmd(.now, cmd.obfuscated)) ChatModel.shared.terminalItems.append(.resp(.now, resp)) } return resp @@ -185,6 +185,10 @@ func apiDeleteStorage() async throws { try await sendCommandOkResp(.apiDeleteStorage) } +func apiStorageEncryption(currentKey: String = "", newKey: String = "") async throws { + try await sendCommandOkResp(.apiStorageEncryption(config: DBEncryptionConfig(currentKey: currentKey, newKey: newKey))) +} + func apiGetChats() throws -> [ChatData] { let r = chatSendCmdSync(.apiGetChats) if case let .apiChats(chats) = r { return chats } @@ -654,22 +658,21 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws throw r } -func initializeChat(start: Bool) throws { +func initializeChat(start: Bool, dbKey: String? = nil) throws { logger.debug("initializeChat") - do { - let m = ChatModel.shared - try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) - try apiSetIncognito(incognito: incognitoGroupDefault.get()) - m.currentUser = try apiGetActiveUser() - if m.currentUser == nil { - m.onboardingStage = .step1_SimpleXInfo - } else if start { - try startChat() - } else { - m.chatRunning = false - } - } catch { - fatalError("Failed to initialize chat controller or database: \(responseError(error))") + let m = ChatModel.shared + (m.chatDbEncrypted, m.chatDbStatus) = migrateChatDatabase(dbKey) + if m.chatDbStatus != .ok { return } + let _ = getChatCtrl(dbKey) + try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + try apiSetIncognito(incognito: incognitoGroupDefault.get()) + m.currentUser = try apiGetActiveUser() + if m.currentUser == nil { + m.onboardingStage = .step1_SimpleXInfo + } else if start { + try startChat() + } else { + m.chatRunning = false } } diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index 2c0261ae8f..33ad5af8ed 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -71,9 +71,9 @@ private func _chatSuspended() { } } -func activateChat(appState: AppState = .active) { +func activateChat(appState: AppState = .active, databaseReady: Bool = true) { suspendLockQueue.sync { appStateGroupDefault.set(appState) - apiActivateChat() + if databaseReady { apiActivateChat() } } } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 386ae0c431..6061e7ce52 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -64,7 +64,7 @@ struct SimpleXApp: App { ChatReceiver.shared.start() } let appState = appStateGroupDefault.get() - activateChat() + activateChat(databaseReady: chatModel.chatDbStatus == .ok) if appState.inactive && chatModel.chatRunning == true { updateChats() updateCallInvitations() diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift new file mode 100644 index 0000000000..374133355b --- /dev/null +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -0,0 +1,343 @@ +// +// DatabaseEncryptionView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 04/09/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +enum DatabaseEncryptionAlert: Identifiable { + case keychainRemoveKey + case encryptDatabaseSaved + case encryptDatabase + case changeDatabaseKeySaved + case changeDatabaseKey + case databaseEncrypted + case currentPassphraseError + case error(title: LocalizedStringKey, error: String = "") + + var id: String { + switch self { + case .keychainRemoveKey: return "keychainRemoveKey" + case .encryptDatabaseSaved: return "encryptDatabaseSaved" + case .encryptDatabase: return "encryptDatabase" + case .changeDatabaseKeySaved: return "changeDatabaseKeySaved" + case .changeDatabaseKey: return "changeDatabaseKey" + case .databaseEncrypted: return "databaseEncrypted" + case .currentPassphraseError: return "currentPassphraseError" + case let .error(title, _): return "error \(title)" + } + } +} + +struct DatabaseEncryptionView: View { + @EnvironmentObject private var m: ChatModel + @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 + @State private var currentKey = "" + @State private var newKey = "" + @State private var confirmNewKey = "" + @State private var currentKeyShown = false + + var body: some View { + ZStack { + databaseEncryptionView() + if progressIndicator { + ProgressView().scaleEffect(2) + } + } + } + + private func databaseEncryptionView() -> some View { + List { + Section { + settingsRow("key") { + Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle) + .onChange(of: useKeychainToggle) { _ in + if useKeychainToggle { + setUseKeychain(true) + } else if storedKey { + alert = .keychainRemoveKey + } else { + setUseKeychain(false) + } + } + .disabled(initialRandomDBPassphrase) + } + + if !initialRandomDBPassphrase && m.chatDbEncrypted == true { + DatabaseKeyField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey)) + } + + DatabaseKeyField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true) + DatabaseKeyField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey) + + settingsRow("lock.rotation") { + Button("Update database passphrase") { + alert = currentKey == "" + ? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase) + : (useKeychain ? .changeDatabaseKeySaved : .changeDatabaseKey) + } + } + .disabled( + currentKey == newKey || + newKey != confirmNewKey || + newKey == "" || + !validKey(currentKey) || + !validKey(newKey) + ) + } header: { + Text("") + } footer: { + VStack(alignment: .leading, spacing: 16) { + if m.chatDbEncrypted == false { + Text("Your chat database is not encrypted - set passphrase to encrypt it.") + } else if useKeychain { + if storedKey { + Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.") + if initialRandomDBPassphrase { + Text("Database is encrypted using a random passphrase, you can change it.") + } else { + Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.") + } + } else { + Text("iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.") + } + } else { + Text("You have to enter passphrase every time the app starts - it is not stored on the device.") + Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.") + if m.notificationMode == .instant && m.notificationPreview != .hidden { + Text("**Warning**: Instant push notifications require passphrase saved in Keychain.") + } + } + } + .padding(.top, 1) + .font(.callout) + } + } + .onAppear { + if initialRandomDBPassphrase { currentKey = getDatabaseKey() ?? "" } + } + .disabled(m.chatRunning != false) + .alert(item: $alert) { item in databaseEncryptionAlert(item) } + } + + private func encryptDatabase() { + progressIndicator = true + Task { + do { + try await apiStorageEncryption(currentKey: currentKey, newKey: newKey) + initialRandomDBPassphraseGroupDefault.set(false) + if useKeychain { + if setDatabaseKey(newKey) { + await resetFormAfterEncryption(true) + await operationEnded(.databaseEncrypted) + } else { + await resetFormAfterEncryption() + await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain")) + } + } else { + await resetFormAfterEncryption() + await operationEnded(.databaseEncrypted) + } + } catch let error { + if case .chatCmdError(.errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse { + await operationEnded(.currentPassphraseError) + } else { + await operationEnded(.error(title: "Error encrypting database", error: responseError(error))) + } + } + } + } + + private func resetFormAfterEncryption(_ stored: Bool = false) async { + await MainActor.run { + m.chatDbEncrypted = true + initialRandomDBPassphrase = false + currentKey = "" + newKey = "" + confirmNewKey = "" + storedKey = stored + } + } + + private func setUseKeychain(_ value: Bool) { + useKeychain = value + storeDBPassphraseGroupDefault.set(value) + } + + private func databaseEncryptionAlert(_ alertItem: DatabaseEncryptionAlert) -> Alert { + switch alertItem { + case .keychainRemoveKey: + return Alert( + title: Text("Remove passphrase from keychain?"), + message: Text("Instant push notifications will be hidden!\n") + storeSecurelyDanger(), + primaryButton: .destructive(Text("Remove")) { + if removeDatabaseKey() { + setUseKeychain(false) + storedKey = false + } else { + alert = .error(title: "Keychain error", error: "Failed to remove passphrase") + } + }, + secondaryButton: .cancel() { + withAnimation { useKeychainToggle = true } + } + ) + case .encryptDatabaseSaved: + return Alert( + title: Text("Encrypt database?"), + message: Text("Database will be encrypted and the passphrase stored in the keychain.\n") + storeSecurelySaved(), + primaryButton: .default(Text("Encrypt")) { encryptDatabase() }, + secondaryButton: .cancel() + ) + case .encryptDatabase: + return Alert( + title: Text("Encrypt database?"), + message: Text("Database will be encrypted.\n") + storeSecurelyDanger(), + primaryButton: .destructive(Text("Encrypt")) { encryptDatabase() }, + secondaryButton: .cancel() + ) + case .changeDatabaseKeySaved: + return Alert( + title: Text("Change database passphrase?"), + message: Text("Database encryption passphrase will be updated and stored in the keychain.\n") + storeSecurelySaved(), + primaryButton: .default(Text("Update")) { encryptDatabase() }, + secondaryButton: .cancel() + ) + case .changeDatabaseKey: + return Alert( + title: Text("Change database passphrase?"), + message: Text("Database encryption passphrase will be updated.\n") + storeSecurelyDanger(), + primaryButton: .destructive(Text("Update")) { encryptDatabase() }, + secondaryButton: .cancel() + ) + case .databaseEncrypted: + return Alert(title: Text("Database encrypted!")) + case .currentPassphraseError: + return Alert( + title: Text("Wrong passsphrase!"), + message: Text("Please enter correct current passphrase") + ) + case let .error(title, error): + return Alert(title: Text(title), message: Text("\(error)")) + } + } + + private func storeSecurelySaved() -> Text { + Text("Please store passphrase securely, you will NOT be able to change it if you lose it.") + } + + private func storeSecurelyDanger() -> Text { + Text("Please store passphrase securely, you will NOT be able to access chat if you lose it.") + } + + private func operationEnded(_ dbAlert: DatabaseEncryptionAlert) async { + await MainActor.run { + m.chatDbChanged = true + progressIndicator = false + alert = dbAlert + } + } +} + + +struct DatabaseKeyField: View { + @Binding var key: String + var placeholder: LocalizedStringKey + var valid: Bool + var showStrength = false + @State private var showKey = false + + var body: some View { + ZStack(alignment: .leading) { + let iconColor = valid + ? (showStrength && key != "" ? PassphraseStrength(passphrase: key).color : .secondary) + : .red + Image(systemName: valid ? (showKey ? "eye.slash" : "eye") : "exclamationmark.circle") + .resizable() + .scaledToFit() + .frame(width: 20, height: 22, alignment: .center) + .foregroundColor(iconColor) + .onTapGesture { showKey = !showKey } + textField() + .disableAutocorrection(true) + .autocapitalization(.none) + .submitLabel(.done) + .padding(.leading, 36) + } + } + + @ViewBuilder func textField() -> some View { + if showKey { + TextField(placeholder, text: $key) + } else { + SecureField(placeholder, text: $key) + } + } +} + +// based on https://generatepasswords.org/how-to-calculate-entropy/ +private func passphraseEnthropy(_ s: String) -> Double { + var hasDigits = false + var hasUppercase = false + var hasLowercase = false + var hasSymbols = false + for c in s { + if c.isNumber { + hasDigits = true + } else if c.isLetter { + if c.isUppercase { hasUppercase = true } + else { hasLowercase = true } + } else if c.isASCII { + hasSymbols = true + } + } + let poolSize: Double = (hasDigits ? 10 : 0) + (hasUppercase ? 26 : 0) + (hasLowercase ? 26 : 0) + (hasSymbols ? 32 : 0) + return Double(s.count) * log2(poolSize) +} + +enum PassphraseStrength { + case veryWeak + case weak + case reasonable + case strong + + init(passphrase s: String) { + let enthropy = passphraseEnthropy(s) + self = enthropy > 60 + ? .strong + : enthropy > 45 + ? .reasonable + : enthropy > 30 + ? .weak + : .veryWeak + } + + var color: Color { + switch self { + case .veryWeak: return .red + case .weak: return .orange + case .reasonable: return .yellow + case .strong: return .green + } + } +} + +func validKey(_ s: String) -> Bool { + for c in s { if c.isWhitespace || !c.isASCII { return false } } + return true +} + +struct DatabaseEncryptionView_Previews: PreviewProvider { + static var previews: some View { + DatabaseEncryptionView() + } +} diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift new file mode 100644 index 0000000000..310c0d96ea --- /dev/null +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -0,0 +1,93 @@ +// +// DatabaseErrorView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 04/09/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct DatabaseErrorView: View { + @EnvironmentObject var m: ChatModel + var status: DBMigrationResult + @State private var dbKey = "" + @State private var storedDBKey = getDatabaseKey() + @State private var useKeychain = storeDBPassphraseGroupDefault.get() + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + switch status { + case let .errorNotADatabase(dbFile): + 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)) + 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 { + saveAndOpenButton() + } else { + openChatButton() + } + Spacer() + } + case let .error(dbFile, migrationError): + Text("Database error") + .font(.title) + Text("File: \(dbFile)") + Text("Error: \(migrationError)") + Spacer() + case .errorKeychain: + Text("Keychain error") + .font(.title) + Text("Cannot access keychain to save database password") + Spacer() + case let .unknown(json): + Text("Database error") + .font(.title) + Text("Unknown database error: \(json)") + Spacer() + case .ok: + EmptyView() + } + } + .padding() + .frame(maxHeight: .infinity) } + + 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))") + } + } + } + + 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))") + } + } + } +} + +struct DatabaseErrorView_Previews: PreviewProvider { + static var previews: some View { + DatabaseErrorView(status: .errorNotADatabase(dbFile: "simplex_v1_chat.db")) + } +} diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index d904cd05ca..8f7ba7cca4 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -11,6 +11,7 @@ import SimpleXChat enum DatabaseAlert: Identifiable { case stopChat + case exportProhibited case importArchive case archiveImported case deleteChat @@ -21,6 +22,7 @@ enum DatabaseAlert: Identifiable { var id: String { switch self { case .stopChat: return "stopChat" + case .exportProhibited: return "exportProhibited" case .importArchive: return "importArchive" case .archiveImported: return "archiveImported" case .deleteChat: return "deleteChat" @@ -82,18 +84,28 @@ struct DatabaseView: View { } Section { - settingsRow("square.and.arrow.up") { - Button { - exportArchive() + let unencrypted = m.chatDbEncrypted == false + let color: Color = unencrypted ? .orange : .secondary + settingsRow(unencrypted ? "lock.open" : "lock", color: color) { + NavigationLink { + DatabaseEncryptionView() + .navigationTitle("Database passphrase") } label: { - Text("Export database") + Text("Database passphrase") + } + } + settingsRow("square.and.arrow.up") { + Button("Export database") { + if initialRandomDBPassphraseGroupDefault.get() { + alert = .exportProhibited + } else { + exportArchive() + } } } settingsRow("square.and.arrow.down") { - Button(role: .destructive) { + Button("Import database", role: .destructive) { showFileImporter = true - } label: { - Text("Import database") } } if let archiveName = chatArchiveName { @@ -110,10 +122,8 @@ struct DatabaseView: View { } } settingsRow("trash.slash") { - Button(role: .destructive) { + Button("Delete database", role: .destructive) { alert = .deleteChat - } label: { - Text("Delete database") } } } header: { @@ -130,10 +140,8 @@ struct DatabaseView: View { if case .group = dbContainer, legacyDatabase { Section("Old database") { settingsRow("trash") { - Button { + Button("Delete old database") { alert = .deleteLegacyDatabase - } label: { - Text("Delete old database") } } } @@ -166,6 +174,11 @@ struct DatabaseView: View { withAnimation { runChat = true } } ) + case .exportProhibited: + return Alert( + title: Text("Set passphrase to export"), + message: Text("Database is encrypted using a random passphrase. Please change it before exporting.") + ) case .importArchive: if let fileURL = importedArchivePath { return Alert( @@ -254,6 +267,7 @@ struct DatabaseView: View { do { let config = ArchiveConfig(archivePath: archivePath.path) try await apiImportArchive(config: config) + _ = removeDatabaseKey() await operationEnded(.archiveImported) } catch let error { await operationEnded(.error(title: "Error importing chat database", error: responseError(error))) @@ -273,6 +287,8 @@ struct DatabaseView: View { Task { do { try await apiDeleteStorage() + _ = removeDatabaseKey() + storeDBPassphraseGroupDefault.set(true) await operationEnded(.chatDeleted) } catch let error { await operationEnded(.error(title: "Error deleting database", error: responseError(error))) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 3a2fe30ae5..2fff1adc92 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -44,7 +44,7 @@ let appDefaults: [String: Any] = [ DEFAULT_ACCENT_COLOR_RED: 0.000, DEFAULT_ACCENT_COLOR_GREEN: 0.533, DEFAULT_ACCENT_COLOR_BLUE: 1.000, - DEFAULT_USER_INTERFACE_STYLE: 0 + DEFAULT_USER_INTERFACE_STYLE: 0, ] private var indent: CGFloat = 36 @@ -92,9 +92,10 @@ struct SettingsView: View { DatabaseView(showSettings: $showSettings) .navigationTitle("Your chat database") } label: { - settingsRow("internaldrive") { + let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary + settingsRow("internaldrive", color: color) { HStack { - Text("Database export & import") + Text("Database passphrase & export") Spacer() if chatModel.chatRunning == false { Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red) diff --git a/apps/ios/SimpleX (iOS).entitlements b/apps/ios/SimpleX (iOS).entitlements index 6dda31ceba..51672d6290 100644 --- a/apps/ios/SimpleX (iOS).entitlements +++ b/apps/ios/SimpleX (iOS).entitlements @@ -14,5 +14,9 @@ group.chat.simplex.app + keychain-access-groups + + $(AppIdentifierPrefix)chat.simplex.app + diff --git a/apps/ios/SimpleX NSE/SimpleX NSE.entitlements b/apps/ios/SimpleX NSE/SimpleX NSE.entitlements index 82cf32be67..51dea2c806 100644 --- a/apps/ios/SimpleX NSE/SimpleX NSE.entitlements +++ b/apps/ios/SimpleX NSE/SimpleX NSE.entitlements @@ -6,5 +6,9 @@ group.chat.simplex.app + keychain-access-groups + + $(AppIdentifierPrefix)chat.simplex.app + diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 060d05d5f9..46cfbb2b8c 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -13,11 +13,12 @@ 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; }; 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; }; 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; }; - 5C00166A28C119300094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166528C119300094D739 /* libgmp.a */; }; - 5C00166B28C119300094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166628C119300094D739 /* libffi.a */; }; - 5C00166C28C119300094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166728C119300094D739 /* libgmpxx.a */; }; - 5C00166D28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */; }; - 5C00166E28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */; }; + 5C00167528C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */; }; + 5C00167728C28A6B0094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167028C28A6B0094D739 /* libgmpxx.a */; }; + 5C00167928C28A6B0094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167128C28A6B0094D739 /* libgmp.a */; }; + 5C00167B28C28A6B0094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167228C28A6B0094D739 /* libffi.a */; }; + 5C00167D28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167328C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a */; }; + 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; }; 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; @@ -61,6 +62,8 @@ 5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */; }; 5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */; }; 5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */; }; + 5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */; }; + 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; @@ -197,11 +200,12 @@ 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = ""; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; }; 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; - 5C00166528C119300094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C00166628C119300094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C00166728C119300094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a"; sourceTree = ""; }; - 5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a"; sourceTree = ""; }; + 5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a"; sourceTree = ""; }; + 5C00167028C28A6B0094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C00167128C28A6B0094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C00167228C28A6B0094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C00167328C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a"; sourceTree = ""; }; + 5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = ""; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; @@ -247,6 +251,8 @@ 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupProfileView.swift; sourceTree = ""; }; 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedNetworkSettings.swift; sourceTree = ""; }; 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAndServers.swift; sourceTree = ""; }; + 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseErrorView.swift; sourceTree = ""; }; + 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = ""; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = ""; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = ""; }; @@ -351,13 +357,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C00166E28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a in Frameworks */, + 5C00167728C28A6B0094D739 /* libgmpxx.a in Frameworks */, + 5C00167B28C28A6B0094D739 /* libffi.a in Frameworks */, + 5C00167D28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a in Frameworks */, + 5C00167928C28A6B0094D739 /* libgmp.a in Frameworks */, + 5C00167528C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C00166C28C119300094D739 /* libgmpxx.a in Frameworks */, - 5C00166A28C119300094D739 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C00166B28C119300094D739 /* libffi.a in Frameworks */, - 5C00166D28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -412,11 +418,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C00166628C119300094D739 /* libffi.a */, - 5C00166528C119300094D739 /* libgmp.a */, - 5C00166728C119300094D739 /* libgmpxx.a */, - 5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */, - 5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */, + 5C00167228C28A6B0094D739 /* libffi.a */, + 5C00167128C28A6B0094D739 /* libgmp.a */, + 5C00167028C28A6B0094D739 /* libgmpxx.a */, + 5C00167328C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a */, + 5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */, ); path = Libraries; sourceTree = ""; @@ -596,6 +602,7 @@ 5CDCAD7D2818941F00503DA2 /* API.swift */, 5CDCAD80281A7E2700503DA2 /* Notifications.swift */, 64DAE1502809D9F5000DA960 /* FileUtils.swift */, + 5C00168028C4FE760094D739 /* KeyChain.swift */, 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */, 5CE2BA8A2845332200EC33A6 /* SimpleX.h */, 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */, @@ -642,6 +649,8 @@ 5C4B3B09285FB130003915F2 /* DatabaseView.swift */, 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */, 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */, + 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */, + 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */, ); path = Database; sourceTree = ""; @@ -858,6 +867,7 @@ 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, 5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */, 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */, + 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */, 5C029EAA283942EA004A9677 /* CallController.swift in Sources */, @@ -934,6 +944,7 @@ 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */, 5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */, 5C3F1D5A2844B4DE00EC8A82 /* ExperimentalFeaturesView.swift in Sources */, + 5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */, 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */, 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */, 5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */, @@ -962,6 +973,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */, 5CE2BA97284537A800EC33A6 /* dummy.m in Sources */, 5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */, 5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */, diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index f718305d86..45362a0de7 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -10,16 +10,46 @@ import Foundation private var chatController: chat_ctrl? -public func getChatCtrl() -> chat_ctrl { +public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl { if let controller = chatController { return controller } let dbPath = getAppDatabasePath().path + let dbKey = useKey ?? getDatabaseKey() ?? "" logger.debug("getChatCtrl DB path: \(dbPath)") - var cstr = dbPath.cString(using: .utf8)! - chatController = chat_init(&cstr) - logger.debug("getChatCtrl: chat_init") + var cPath = dbPath.cString(using: .utf8)! + var cKey = dbKey.cString(using: .utf8)! + chatController = chat_init_key(&cPath, &cKey) + logger.debug("getChatCtrl: chat_init_key") return chatController! } +public func migrateChatDatabase(_ useKey: String? = nil) -> (Bool, DBMigrationResult) { + logger.debug("migrateChatDatabase \(storeDBPassphraseGroupDefault.get())") + let dbPath = getAppDatabasePath().path + var dbKey = "" + let useKeychain = storeDBPassphraseGroupDefault.get() + if let key = useKey { + dbKey = key + } else if useKeychain { + if !hasDatabase() { + dbKey = randomDatabasePassword() + initialRandomDBPassphraseGroupDefault.set(true) + } else if let key = getDatabaseKey() { + dbKey = key + } + } + logger.debug("migrateChatDatabase DB path: \(dbPath)") +// logger.debug("migrateChatDatabase DB key: \(dbKey)") + var cPath = dbPath.cString(using: .utf8)! + var cKey = dbKey.cString(using: .utf8)! + let cjson = chat_migrate_db(&cPath, &cKey)! + let res = dbMigrationResult(fromCString(cjson)) + let encrypted = dbKey != "" + if case .ok = res, useKeychain && encrypted && !setDatabaseKey(dbKey) { + return (encrypted, .errorKeychain) + } + return (encrypted, res) +} + public func resetChatCtrl() { chatController = nil } @@ -103,3 +133,24 @@ public func responseError(_ err: Error) -> String { return err.localizedDescription } } + +public enum DBMigrationResult: Decodable, Equatable { + case ok + case errorNotADatabase(dbFile: String) + case error(dbFile: String, migrationError: String) + case errorKeychain + case unknown(json: String) +} + +func dbMigrationResult(_ s: String) -> DBMigrationResult { + let d = s.data(using: .utf8)! +// TODO is there a way to do it without copying the data? e.g: +// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) +// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free) + do { + return try jsonDecoder.decode(DBMigrationResult.self, from: d) + } catch let error { + logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") + return .unknown(json: s) + } +} diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 2299bd899a..6d548e86f3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -24,6 +24,7 @@ public enum ChatCommand { case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage + case apiStorageEncryption(config: DBEncryptionConfig) case apiGetChats case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) @@ -88,6 +89,7 @@ public enum ChatCommand { case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" + case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))" case .apiGetChats: return "/_get chats pcc=on" case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") @@ -156,6 +158,7 @@ public enum ChatCommand { case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" case .apiDeleteStorage: return "apiDeleteStorage" + case .apiStorageEncryption: return "apiStorageEncryption" case .apiGetChats: return "apiGetChats" case .apiGetChat: return "apiGetChat" case .apiSendMessage: return "apiSendMessage" @@ -214,6 +217,18 @@ public enum ChatCommand { func smpServersStr(smpServers: [String]) -> String { smpServers.isEmpty ? "default" : smpServers.joined(separator: ",") } + + public var obfuscated: ChatCommand { + switch self { + case let .apiStorageEncryption(cfg): + return .apiStorageEncryption(config: DBEncryptionConfig(currentKey: obfuscate(cfg.currentKey), newKey: obfuscate(cfg.newKey))) + default: return self + } + } + + private func obfuscate(_ s: String) -> String { + s == "" ? "" : "***" + } } struct APIResponse: Decodable { @@ -527,6 +542,16 @@ public struct ArchiveConfig: Encodable { } } +public struct DBEncryptionConfig: Encodable { + public init(currentKey: String, newKey: String) { + self.currentKey = currentKey + self.newKey = newKey + } + + public var currentKey: String + public var newKey: String +} + public struct NetCfg: Codable, Equatable { public var socksProxy: String? = nil public var hostMode: HostMode = .publicHost @@ -710,6 +735,7 @@ public enum ChatError: Decodable { case error(errorType: ChatErrorType) case errorAgent(agentError: AgentErrorType) case errorStore(storeError: StoreError) + case errorDatabase(databaseError: DatabaseError) } public enum ChatErrorType: Decodable { @@ -779,6 +805,19 @@ public enum StoreError: Decodable { case chatItemNotFoundByFileId(fileId: Int64) } +public enum DatabaseError: Decodable { + case errorEncrypted + case errorPlaintext + case errorNoFile(dbFile: String) + case errorExport(sqliteError: SQLiteError) + case errorOpen(sqliteError: SQLiteError) +} + +public enum SQLiteError: Decodable { + case errorNotADatabase + case error(String) +} + public enum AgentErrorType: Decodable { case CMD(cmdErr: CommandErrorType) case CONN(connErr: ConnectionErrorType) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index d428c00832..c44ed18af9 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -24,6 +24,8 @@ let GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE = "networkTCPKeepIdle" let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl" let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt" let GROUP_DEFAULT_INCOGNITO = "incognito" +let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase" +let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" let APP_GROUP_NAME = "group.chat.simplex.app" @@ -39,7 +41,9 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE: KeepAliveOpts.defaults.keepIdle, GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL: KeepAliveOpts.defaults.keepIntvl, GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt, - GROUP_DEFAULT_INCOGNITO: false + GROUP_DEFAULT_INCOGNITO: false, + GROUP_DEFAULT_STORE_DB_PASSPHRASE: true, + GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false ]) } @@ -96,6 +100,10 @@ public let networkUseOnionHostsGroupDefault = EnumDefault( withDefault: .no ) +public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE) + +public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE) + public class DateDefault { var defaults: UserDefaults var key: String diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index b236712910..326741d121 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -42,11 +42,17 @@ public func getAppDatabasePath() -> URL { dbContainerGroupDefault.get() == .group ? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX, isDirectory: false) : getLegacyDatabasePath() -// getLegacyDatabasePath() } public func hasLegacyDatabase() -> Bool { - let dbPath = getLegacyDatabasePath() + hasDatabaseAtPath(getLegacyDatabasePath()) +} + +public func hasDatabase() -> Bool { + hasDatabaseAtPath(getAppDatabasePath()) +} + +func hasDatabaseAtPath(_ dbPath: URL) -> Bool { let fm = FileManager.default return fm.isReadableFile(atPath: dbPath.path + "_agent.db") && fm.isReadableFile(atPath: dbPath.path + "_chat.db") diff --git a/apps/ios/SimpleXChat/KeyChain.swift b/apps/ios/SimpleXChat/KeyChain.swift new file mode 100644 index 0000000000..704c5f752c --- /dev/null +++ b/apps/ios/SimpleXChat/KeyChain.swift @@ -0,0 +1,107 @@ +// +// KeyChain.swift +// SimpleXChat +// +// Created by Evgeny on 04/09/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import Security + +private let ACCESS_POLICY: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly +private let ACCESS_GROUP: String = "5NN7GUYB6T.chat.simplex.app" +private let DATABASE_PASSWORD_ITEM: String = "databasePassword" + +public func getDatabaseKey() -> String? { + getItemString(forKey: DATABASE_PASSWORD_ITEM) +} + +public func setDatabaseKey(_ key: String) -> Bool { + setItemString(key, forKey: DATABASE_PASSWORD_ITEM) +} + +public func removeDatabaseKey() -> Bool { + deleteItem(forKey: DATABASE_PASSWORD_ITEM) +} + +func randomDatabasePassword() -> String { + var keyData = Data(count: 32) + let status = keyData.withUnsafeMutableBytes { + SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!) + } + if status == errSecSuccess { + return keyData.base64EncodedString() + } else { + logger.error("randomDatabasePassword: error \(status)") + return "" + } +} + +private func getItemData(forKey key: String) -> Data? { + var query = baseItemQuery(forKey: key) + query[kSecMatchLimit] = kSecMatchLimitOne + query[kSecReturnData] = true as AnyObject? + + var dataRef: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &dataRef) + if status != errSecSuccess && status != errSecItemNotFound { + logger.error("getItemData: error getting data for key '\(key)', error: \(status)") + } + return dataRef as? Data +} + +private func getItemString(forKey key: String) -> String? { + if let data = getItemData(forKey: key) { + return NSString(data: data, encoding: String.Encoding.utf8.rawValue) as? String + } + return nil +} + +private func setItemData(_ data: Data, forKey key: String) -> Bool { + var query = baseItemQuery(forKey: key) + var update = [NSString : AnyObject]() + update[kSecValueData] = data as AnyObject? + update[kSecAttrAccessible] = ACCESS_POLICY + var status: OSStatus + if getItemData(forKey: key) == nil { + for (key, value) in update { query[key] = value } + status = SecItemAdd(query as CFDictionary, nil) + } else { + status = SecItemUpdate(query as CFDictionary, update as CFDictionary) + } + if status != errSecSuccess { + logger.error("setItemData: error setting data for key '\(key)', error: \(status)") + return false + } + return true +} + +private func setItemString(_ s: String, forKey key: String) -> Bool { + if let data = s.data(using: .utf8) { + return setItemData(data, forKey: key) + } + return false +} + +private func deleteItem(forKey key: String) -> Bool { + let query = baseItemQuery(forKey: key) + if getItemData(forKey: key) != nil { + let status = SecItemDelete(query as CFDictionary) + if status != errSecSuccess { + logger.error("deleteItem: error deleting data for key '\(key)', error: \(status)") + return false + } + } + return true +} + +private func baseItemQuery(forKey key: String) -> [NSString : AnyObject] { + var query = [NSString : AnyObject]() + query[kSecClass] = kSecClassGenericPassword + query[kSecAttrAccount] = key as AnyObject? + #if TARGET_OS_IOS && !TARGET_OS_SIMULATOR + query[kSecAttrAccessGroup] = ACCESS_GROUP + #endif + return query +} diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index e848ad5cdd..1b2f5d821b 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -15,7 +15,8 @@ extern void hs_init(int argc, char **argv[]); typedef void* chat_ctrl; -extern chat_ctrl chat_init(char *path); +extern char *chat_migrate_db(char *path, char *key); +extern chat_ctrl chat_init_key(char *path, char *key); extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); extern char *chat_recv_msg(chat_ctrl ctl); extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);