mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-06 23:39:54 +00:00
ios: use SQLCipher (#1012)
* use sqlcipher build, hardcoded encryption key * UI for db encryption * database passphrase UI * show orange icon when database is not encrypted * call encrypt * more ios ux * basic UX for passphrase complete * with animation Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * passphrase complexity, fixes * fix moving entry field Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
This commit is contained in:
co-authored by
JRoberts
parent
00471a095d
commit
766009269e
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -14,5 +14,9 @@
|
||||
<array>
|
||||
<string>group.chat.simplex.app</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)chat.simplex.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>group.chat.simplex.app</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)chat.simplex.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
|
||||
5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; };
|
||||
5C00166528C119300094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C00166628C119300094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C00166728C119300094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a"; sourceTree = "<group>"; };
|
||||
5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a"; sourceTree = "<group>"; };
|
||||
5C00167028C28A6B0094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C00167128C28A6B0094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C00167228C28A6B0094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = "<group>"; };
|
||||
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
|
||||
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
|
||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
|
||||
@@ -247,6 +251,8 @@
|
||||
5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupProfileView.swift; sourceTree = "<group>"; };
|
||||
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedNetworkSettings.swift; sourceTree = "<group>"; };
|
||||
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAndServers.swift; sourceTree = "<group>"; };
|
||||
5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseErrorView.swift; sourceTree = "<group>"; };
|
||||
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
|
||||
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
|
||||
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -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 = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<OnionHosts>(
|
||||
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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user