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:
Evgeny Poberezkin
2022-09-07 12:49:41 +01:00
committed by GitHub
co-authored by JRoberts
parent 00471a095d
commit 766009269e
18 changed files with 756 additions and 63 deletions
+3
View File
@@ -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 {
+2
View File
@@ -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
+19 -16
View File
@@ -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
}
}
+2 -2
View File
@@ -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() }
}
}
+1 -1
View File
@@ -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)
+4
View File
@@ -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>
+32 -20
View File
@@ -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 */,
+55 -4
View File
@@ -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)
}
}
+39
View File
@@ -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)
+9 -1
View File
@@ -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
+8 -2
View File
@@ -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")
+107
View File
@@ -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
}
+2 -1
View File
@@ -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);