Browse Source

Encrypt per account, adapt keychain handling accordingly. Improve error handling.

cyberta 3 years ago
parent
commit
69a953ccb2

+ 38 - 34
DcCore/DcCore/Helper/KeychainManager.swift

@@ -1,31 +1,35 @@
 import Foundation
 import Security
 
+public enum KeychainError: Error {
+    case noPassword
+    case unhandledError(message: String, status: OSStatus)
+}
+
 public class KeychainManager {
-    enum KeychainError: Error {
-        case noPassword
-        case unexpectedPasswordData
-        case unhandledError(status: OSStatus)
-    }
     private typealias KcM = KeychainManager
     private static let teamId = "8Y86453UA8"
     private static let sharedKeychainGroup = "\(KcM.teamId).group.chat.delta.ios"
 
-    public static func getDBSecret() throws -> String {
-        guard let secret = try? queryDBSecret() else {
-            return try addDBSecret()
+    public static func getAccountSecret(accountID: Int) throws -> String {
+        do {
+            return try queryAccountSecret(id: accountID)
+        } catch KeychainError.noPassword {
+            return try addAccountSecret(id: accountID)
         }
-        
-        return secret
     }
 
-    public static func deleteDBSecret() -> Bool {
+    /**
+     * Deletes ALL secrets from keychain
+     * @return true if secrets have been deleted successfully or no secrets found
+     */
+    public static func deleteDBSecrets() -> Bool {
         let query = [kSecClass as String: kSecClassGenericPassword,
-                     kSecAttrAccount as String: "dc_db"
+                     kSecAttrAccessGroup as String: KcM.sharedKeychainGroup as AnyObject
                     ] as CFDictionary
 
         let status = SecItemDelete(query)
-        return status == errSecSuccess
+        return status == errSecSuccess || status == errSecItemNotFound
     }
 
     private static func createRandomPassword() -> String {
@@ -33,47 +37,47 @@ public class KeychainManager {
         return String((0..<36).map { _ in letters.randomElement()! })
     }
 
-    private static func addDBSecret() throws -> String {
+    private static func addAccountSecret(id: Int) throws -> String {
         let keychainItemQuery = [
           kSecValueData: createRandomPassword().data(using: .utf8)!,
-          kSecAttrAccount as String: "dc_db",
+          kSecAttrAccount as String: "\(id)",
           kSecClass: kSecClassGenericPassword,
-          kSecAttrAccessGroup as String: KeychainManager.sharedKeychainGroup as AnyObject
+          kSecAttrAccessGroup as String: KcM.sharedKeychainGroup as AnyObject,
         ] as CFDictionary
 
-        var ref: AnyObject?
-        
-        let status = SecItemAdd(keychainItemQuery, &ref)
-        guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
-        UserDefaults.shared?.set(true, forKey: UserDefaults.hasSavedKeyToKeychain)
-        if let result = ref as? NSDictionary,
-            let password = result[kSecValueData] as? String {
-            return password
+        let status = SecItemAdd(keychainItemQuery, nil)
+        guard status == errSecSuccess else {
+            throw KeychainError.unhandledError(message: "Error adding secret for account \(id)",
+                                               status: status)
         }
-        
-        return try queryDBSecret()
+        UserDefaults.shared?.set(true, forKey: UserDefaults.hasSavedKeyToKeychain)
+        return try queryAccountSecret(id: id)
     }
 
-    private static func queryDBSecret() throws -> String {
+    private static func queryAccountSecret(id: Int) throws -> String {
         let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
-                                    kSecAttrAccount as String: "dc_db",
+                                    kSecAttrAccount as String: "\(id)",
                                     kSecMatchLimit as String: kSecMatchLimitOne,
-                                    kSecAttrAccessGroup as String: KeychainManager.sharedKeychainGroup as AnyObject,
+                                    kSecAttrAccessGroup as String: KcM.sharedKeychainGroup as AnyObject,
                                     kSecReturnAttributes as String: true,
                                     kSecReturnData as String: true]
-        
         var item: CFTypeRef?
         let status = SecItemCopyMatching(query as CFDictionary, &item)
-        guard status != errSecItemNotFound else { throw KeychainError.noPassword }
-        guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
+        guard status != errSecItemNotFound else {
+            throw KeychainError.noPassword
+        }
+        guard status == errSecSuccess else {
+            throw KeychainError.unhandledError(message: "Unknown error while querying secret for account \(id):",
+                                               status: status)
+        }
         
         guard let existingItem = item as? [String: Any],
             let passwordData = existingItem[kSecValueData as String] as? Data,
             let password = String(data: passwordData, encoding: String.Encoding.utf8)
         else {
-            throw KeychainError.unexpectedPasswordData
+            throw KeychainError.unhandledError(message: "Unexpected password data for accuont \(id)",
+                                               status: 0)
         }
-
         return password
     }
 }

+ 9 - 3
DcShare/Controller/ShareViewController.swift

@@ -85,9 +85,15 @@ class ShareViewController: SLComposeServiceViewController {
         dcAccounts.logger = logger
         dcAccounts.openDatabase()
         if !dcContext.isOpen() {
-            let secret = try? KeychainManager.getDBSecret()
-            if !dcContext.open(passphrase: secret) {
-                logger.error("Failed to open database.")
+            do {
+                let secret = try KeychainManager.getAccountSecret(accountID: dcContext.id)
+                if !dcContext.open(passphrase: secret) {
+                    logger.error("Failed to open database.")
+                }
+            } catch KeychainError.unhandledError(let message, let status) {
+                logger.error("KeychainError. \(message). Error status: \(status)")
+            } catch {
+                logger.error("\(error)")
             }
         }
         isAccountConfigured = dcContext.isOpen() && dcContext.isConfigured()

+ 12 - 9
deltachat-ios/AppDelegate.swift

@@ -64,22 +64,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
 
         if let sharedUserDefaults = UserDefaults.shared, !sharedUserDefaults.bool(forKey: UserDefaults.hasSavedKeyToKeychain) {
             // we can assume a fresh install -> reset the keychain for the case the app was removed and reinstalled
-            if !KeychainManager.deleteDBSecret() {
-                logger.warning("Failed to delete DB secret")
+            if !KeychainManager.deleteDBSecrets() {
+                logger.warning("Failed to delete DB secrets")
             }
         }
-        let passphrase: String
-        do {
-            passphrase = try KeychainManager.getDBSecret()
-        } catch {
-            fatalError("Could not get nor create a DB secret")
-        }
 
         let accountIds = dcAccounts.getAll()
         for accountId in accountIds {
             let dcContext = dcAccounts.get(id: accountId)
             if !dcContext.isOpen() {
-                _ = dcContext.open(passphrase: passphrase)
+                do {
+                    let secret = try KeychainManager.getAccountSecret(accountID: accountId)
+                    if !dcContext.open(passphrase: secret) {
+                        logger.error("Failed to open database for account \(accountId)")
+                    }
+                } catch KeychainError.unhandledError(let message, let status) {
+                    logger.error("Keychain error. \(message). Error status: \(status)")
+                } catch {
+                    logger.error("\(error)")
+                }
             }
         }
 

+ 12 - 2
deltachat-ios/Controller/WelcomeViewController.swift

@@ -181,8 +181,18 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
         _ = dcAccounts.remove(id: lastContextId)
         _ = dcAccounts.select(id: newContextId)
         let selected = dcAccounts.getSelected()
-        _ = selected.open(passphrase: try? KeychainManager.getDBSecret())
-        showAccountSetupController()
+        do {
+            let secret = try KeychainManager.getAccountSecret(accountID: selected.id)
+            guard selected.open(passphrase: secret) else {
+                logger.error("Failed to open account database for account \(selected.id)")
+                return
+            }
+            showAccountSetupController()
+        } catch KeychainError.unhandledError(let message, let status) {
+            logger.error("Keychain error. Failed to create encrypted account. \(message). Error status: \(status)")
+        } catch {
+            logger.error("Keychain error. Failed to create encrypted account.")
+        }
     }
 
     private func showAccountSetupController() {