Browse Source

Merge pull request #1438 from deltachat/keychain_handling

encrypted account databases
cyBerta 3 years ago
parent
commit
538d8a3045

+ 4 - 0
DcCore/DcCore.xcodeproj/project.pbxproj

@@ -25,6 +25,7 @@
 		3057028624C5C60000D84EFC /* UITableView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3057028524C5C60000D84EFC /* UITableView+Extensions.swift */; };
 		306C324824460CDE001D89F3 /* DateUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306C324724460CDE001D89F3 /* DateUtils.swift */; };
 		308198AB24866229003BE20D /* UserDefaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 308198AA24866229003BE20D /* UserDefaults+Extensions.swift */; };
+		30B2BD04278F207000889AA4 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2BD03278F207000889AA4 /* KeychainManager.swift */; };
 		30E8F2212447357500CE2C90 /* DatabaseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F2202447357500CE2C90 /* DatabaseHelper.swift */; };
 		30E8F2482449C98600CE2C90 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F2472449C98600CE2C90 /* UIView+Extensions.swift */; };
 		30E8F24B2449CF6500CE2C90 /* InitialsBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F24A2449CF6500CE2C90 /* InitialsBadge.swift */; };
@@ -63,6 +64,7 @@
 		3057028524C5C60000D84EFC /* UITableView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extensions.swift"; sourceTree = "<group>"; };
 		306C324724460CDE001D89F3 /* DateUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUtils.swift; sourceTree = "<group>"; };
 		308198AA24866229003BE20D /* UserDefaults+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Extensions.swift"; sourceTree = "<group>"; };
+		30B2BD03278F207000889AA4 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
 		30E8F2202447357500CE2C90 /* DatabaseHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseHelper.swift; sourceTree = "<group>"; };
 		30E8F2472449C98600CE2C90 /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
 		30E8F24A2449CF6500CE2C90 /* InitialsBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialsBadge.swift; sourceTree = "<group>"; };
@@ -168,6 +170,7 @@
 		30421977243F1AF400516852 /* Helper */ = {
 			isa = PBXGroup;
 			children = (
+				30B2BD03278F207000889AA4 /* KeychainManager.swift */,
 				30E8F2202447357500CE2C90 /* DatabaseHelper.swift */,
 				30421987243F23E500516852 /* Constants.swift */,
 				3042195C243E23F100516852 /* DcUtils.swift */,
@@ -323,6 +326,7 @@
 				30421986243F209E00516852 /* events.swift in Sources */,
 				30421951243DE15D00516852 /* Wrapper.swift in Sources */,
 				306C324824460CDE001D89F3 /* DateUtils.swift in Sources */,
+				30B2BD04278F207000889AA4 /* KeychainManager.swift in Sources */,
 				30421952243DE15D00516852 /* wrapper.c in Sources */,
 				30E8F24B2449CF6500CE2C90 /* InitialsBadge.swift in Sources */,
 				30E8F2212447357500CE2C90 /* DatabaseHelper.swift in Sources */,

+ 15 - 2
DcCore/DcCore/DC/Wrapper.swift

@@ -23,6 +23,10 @@ public class DcAccounts {
         return Int(dc_accounts_migrate_account(accountsPointer, dbLocation))
     }
 
+    public func addClosedAccount() -> Int {
+        return Int(dc_accounts_add_closed_account(accountsPointer))
+    }
+
     public func add() -> Int {
         return Int(dc_accounts_add_account(accountsPointer))
     }
@@ -119,6 +123,15 @@ public class DcContext {
         return swiftString
     }
 
+    // The passphrase can be ommited if the account db is not encrypted
+    public func open(passphrase: String? = nil) -> Bool {
+        dc_context_open(contextPointer, passphrase) == 1
+    }
+
+    public func isOpen() -> Bool {
+        return dc_context_is_open(contextPointer) == 1
+    }
+
     // viewType: one of DC_MSG_*
     public func newMessage(viewType: Int32) -> DcMsg {
         let messagePointer = dc_msg_new(contextPointer, viewType)
@@ -518,8 +531,8 @@ public class DcContext {
         return DcProvider(dcProviderPointer)
     }
 
-    public func imex(what: Int32, directory: String) {
-        dc_imex(contextPointer, what, directory, nil)
+    public func imex(what: Int32, directory: String, passphrase: String? = nil) {
+        dc_imex(contextPointer, what, directory, passphrase)
     }
 
     public func imexHasBackup(filePath: String) -> String? {

+ 1 - 0
DcCore/DcCore/Extensions/UserDefaults+Extensions.swift

@@ -1,6 +1,7 @@
 import Foundation
 public extension UserDefaults {
     static var hasExtensionAttemptedToSend = "hasExtensionAttemptedToSend"
+    static var hasSavedKeyToKeychain = "hasSavedKeyToKeychain"
     static var shared: UserDefaults? {
         return UserDefaults(suiteName: "group.chat.delta.ios")
     }

+ 86 - 0
DcCore/DcCore/Helper/KeychainManager.swift

@@ -0,0 +1,86 @@
+import Foundation
+import Security
+
+public enum KeychainError: Error {
+    case noPassword
+    case unhandledError(message: String, status: OSStatus)
+}
+
+public class KeychainManager {
+    private typealias KcM = KeychainManager
+    // the development team id is equivalent to $(AppIdentifierPrefix) in deltachat-ios.entitlements
+    // It is required as a prefix for the shared keychain identifier, but not straight forward to access programmatically,
+    // so we're hardcoding it here
+    private static let teamId = "8Y86453UA8"
+    private static let sharedKeychainGroup = "\(KcM.teamId).group.chat.delta.ios"
+
+    public static func getAccountSecret(accountID: Int) throws -> String {
+        do {
+            return try queryAccountSecret(id: accountID)
+        } catch KeychainError.noPassword {
+            return try addAccountSecret(id: accountID)
+        }
+    }
+
+    /**
+     * 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,
+                     kSecAttrAccessGroup as String: KcM.sharedKeychainGroup as AnyObject
+                    ] as CFDictionary
+
+        let status = SecItemDelete(query)
+        return status == errSecSuccess || status == errSecItemNotFound
+    }
+
+    private static func createRandomPassword() -> String {
+        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY1234567890"
+        return String((0..<36).map { _ in letters.randomElement()! })
+    }
+
+    private static func addAccountSecret(id: Int) throws -> String {
+        let keychainItemQuery = [
+          kSecValueData: createRandomPassword().data(using: .utf8)!,
+          kSecAttrAccount as String: "\(id)",
+          kSecClass: kSecClassGenericPassword,
+          kSecAttrAccessGroup as String: KcM.sharedKeychainGroup as AnyObject,
+        ] as CFDictionary
+
+        let status = SecItemAdd(keychainItemQuery, nil)
+        guard status == errSecSuccess else {
+            throw KeychainError.unhandledError(message: "Error adding secret for account \(id)",
+                                               status: status)
+        }
+        UserDefaults.shared?.set(true, forKey: UserDefaults.hasSavedKeyToKeychain)
+        return try queryAccountSecret(id: id)
+    }
+
+    private static func queryAccountSecret(id: Int) throws -> String {
+        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+                                    kSecAttrAccount as String: "\(id)",
+                                    kSecMatchLimit as String: kSecMatchLimitOne,
+                                    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(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.unhandledError(message: "Unexpected password data for accuont \(id)",
+                                               status: 0)
+        }
+        return password
+    }
+}

+ 14 - 1
DcShare/Controller/ShareViewController.swift

@@ -84,7 +84,20 @@ class ShareViewController: SLComposeServiceViewController {
     override func presentationAnimationDidFinish() {
         dcAccounts.logger = logger
         dcAccounts.openDatabase()
-        isAccountConfigured = dcContext.isConfigured()
+        if !dcContext.isOpen() {
+            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()
+
         if isAccountConfigured {
             if #available(iOSApplicationExtension 13.0, *) {
                 if let intent = self.extensionContext?.intent as? INSendMessageIntent,

+ 4 - 0
DcShare/DcShare.entitlements

@@ -6,5 +6,9 @@
 	<array>
 		<string>group.chat.delta.ios</string>
 	</array>
+	<key>keychain-access-groups</key>
+	<array>
+		<string>$(AppIdentifierPrefix)group.chat.delta.ios</string>
+	</array>
 </dict>
 </plist>

+ 6 - 0
deltachat-ios.xcodeproj/project.pbxproj

@@ -12,6 +12,7 @@
 		3008CB7424F9436C00E6A617 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */; };
 		3008CB7624F95B6D00E6A617 /* AudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7524F95B6D00E6A617 /* AudioController.swift */; };
 		3010968926838A050032CBA0 /* VideoInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3010968826838A040032CBA0 /* VideoInviteCell.swift */; };
+		3011E8052787365D00214221 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3011E8042787365D00214221 /* KeychainManager.swift */; };
 		30149D9322F21129003C12B5 /* QrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30149D9222F21129003C12B5 /* QrViewController.swift */; };
 		30152C9425A5D91400377714 /* PaddingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F4BFED252E3E020006B9B3 /* PaddingTextView.swift */; };
 		30152C9725A5D91900377714 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EF7323252FF15F00E2C54A /* MessageLabel.swift */; };
@@ -74,6 +75,7 @@
 		30A4149724F6EFBE00EC91EB /* InfoMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A4149624F6EFBE00EC91EB /* InfoMessageCell.swift */; };
 		30AAD71B2762869600DE3DC1 /* SelectableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AAD71A2762869600DE3DC1 /* SelectableCell.swift */; };
 		30B0ACFA24AB5B99004D5E29 /* SettingsEphemeralMessageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0ACF924AB5B99004D5E29 /* SettingsEphemeralMessageController.swift */; };
+		30B2BD02278F1C1900889AA4 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3011E8042787365D00214221 /* KeychainManager.swift */; };
 		30C0D49D237C4908008E2A0E /* CertificateCheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */; };
 		30C2BFFE27032375005505DA /* ChatSearchAccessoryBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C2BFFD27032375005505DA /* ChatSearchAccessoryBar.swift */; };
 		30DAF71C275901610073C154 /* SettingsBackgroundSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DAF71B275901610073C154 /* SettingsBackgroundSelectionController.swift */; };
@@ -219,6 +221,7 @@
 		3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = "<group>"; };
 		3008CB7524F95B6D00E6A617 /* AudioController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
 		3010968826838A040032CBA0 /* VideoInviteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoInviteCell.swift; sourceTree = "<group>"; };
+		3011E8042787365D00214221 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeychainManager.swift; path = ../../DcCore/DcCore/Helper/KeychainManager.swift; sourceTree = "<group>"; };
 		30149D9222F21129003C12B5 /* QrViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrViewController.swift; sourceTree = "<group>"; };
 		3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderController.swift; sourceTree = "<group>"; };
 		3022E6BF22E8768800763272 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -857,6 +860,7 @@
 				AE6EC5272497B9B200A400E4 /* ThumbnailCache.swift */,
 				21D6C9392606190600D0755A /* NotificationManager.swift */,
 				302D544F268B6B2300A8B271 /* MessageUtils.swift */,
+				3011E8042787365D00214221 /* KeychainManager.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -1265,6 +1269,7 @@
 				30152C9D25A5D95400377714 /* MessageLabelDelegate.swift in Sources */,
 				30E8F2442449C64100CE2C90 /* ChatListCell.swift in Sources */,
 				30E8F2132447285600CE2C90 /* ShareViewController.swift in Sources */,
+				30B2BD02278F1C1900889AA4 /* KeychainManager.swift in Sources */,
 				30152C9A25A5D92200377714 /* DetectorType.swift in Sources */,
 				30152CA025A5D97900377714 /* UIEdgeInsets+Extensions.swift in Sources */,
 				3067AAC72667F3FE00525036 /* ImageFormat.swift in Sources */,
@@ -1401,6 +1406,7 @@
 				305DDD8725DD97BF00974489 /* DynamicFontButton.swift in Sources */,
 				785BE16821E247F1003BE98C /* MessageInfoViewController.swift in Sources */,
 				AED423D3249F578B00B6B2BB /* AddGroupMembersViewController.swift in Sources */,
+				3011E8052787365D00214221 /* KeychainManager.swift in Sources */,
 				AE851AC5227C755A00ED86F0 /* Protocols.swift in Sources */,
 				AE728F15229D5C390047565B /* PhotoPickerAlertAction.swift in Sources */,
 				AECEF03E244F2D55006C90DA /* QrPageController.swift in Sources */,

+ 28 - 1
deltachat-ios/AppDelegate.swift

@@ -61,9 +61,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         dcAccounts.logger = DcLogger()
         dcAccounts.openDatabase()
         migrateToDcAccounts()
+
+        if let sharedUserDefaults = UserDefaults.shared, !sharedUserDefaults.bool(forKey: UserDefaults.hasSavedKeyToKeychain) {
+            // we can assume a fresh install (UserDefaults are deleted on app removal)
+            // -> reset the keychain (which survives removals of the app) in case the app was removed and reinstalled.
+            if !KeychainManager.deleteDBSecrets() {
+                logger.warning("Failed to delete DB secrets")
+            }
+        }
+
+        let accountIds = dcAccounts.getAll()
+        for accountId in accountIds {
+            let dcContext = dcAccounts.get(id: accountId)
+            if !dcContext.isOpen() {
+                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)")
+                }
+            }
+        }
+
         if dcAccounts.getAll().isEmpty, dcAccounts.add() == 0 {
            fatalError("Could not initialize a new account.")
         }
+
         logger.info("➡️ didFinishLaunchingWithOptions")
 
         window = UIWindow(frame: UIScreen.main.bounds)
@@ -532,7 +559,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         dcContext.setStockTranslation(id: DC_STR_VIDEO, localizationKey: "video")
         dcContext.setStockTranslation(id: DC_STR_AUDIO, localizationKey: "audio")
         dcContext.setStockTranslation(id: DC_STR_FILE, localizationKey: "file")
-        dcContext.setStockTranslation(id: DC_STR_STATUSLINE, localizationKey: "pref_default_status_text")
+        //dcContext.setStockTranslation(id: DC_STR_STATUSLINE, localizationKey: "pref_default_status_text")
         dcContext.setStockTranslation(id: DC_STR_MSGGRPNAME, localizationKey: "systemmsg_group_name_changed")
         dcContext.setStockTranslation(id: DC_STR_MSGGRPIMGCHANGED, localizationKey: "systemmsg_group_image_changed")
         dcContext.setStockTranslation(id: DC_STR_MSGADDMEMBER, localizationKey: "systemmsg_member_added")

+ 21 - 0
deltachat-ios/Assets.xcassets/ic_more.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "dots-horizontal-circle-outline_3x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
deltachat-ios/Assets.xcassets/ic_more.imageset/dots-horizontal-circle-outline_3x.png


+ 0 - 15
deltachat-ios/Assets.xcassets/ic_more_vert.imageset/Contents.json

@@ -1,15 +0,0 @@
-{
-  "images" : [
-    {
-      "idiom" : "universal",
-      "filename" : "more_vert.pdf"
-    }
-  ],
-  "info" : {
-    "version" : 1,
-    "author" : "xcode"
-  },
-  "properties" : {
-    "preserves-vector-representation" : true
-  }
-}

BIN
deltachat-ios/Assets.xcassets/ic_more_vert.imageset/more_vert.pdf


+ 2 - 2
deltachat-ios/Controller/SettingsController.swift

@@ -594,13 +594,13 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         present(menu, animated: true, completion: nil)
     }
 
-    private func startImex(what: Int32) {
+    private func startImex(what: Int32, passphrase: String? = nil) {
         let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
         if !documents.isEmpty {
             showProgressAlert(title: String.localized("imex_progress_title_desktop"), dcContext: dcContext)
             DispatchQueue.main.async {
                 self.dcAccounts.stopIo()
-                self.dcContext.imex(what: what, directory: documents[0])
+                self.dcContext.imex(what: what, directory: documents[0], passphrase: passphrase)
             }
         } else {
             logger.error("document directory not found")

+ 57 - 8
deltachat-ios/Controller/WelcomeViewController.swift

@@ -17,14 +17,7 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
         let view = WelcomeContentView()
         view.onLogin = { [weak self] in
             guard let self = self else { return }
-            let accountSetupController = AccountSetupController(dcAccounts: self.dcAccounts, editView: false)
-            accountSetupController.onLoginSuccess = {
-                [weak self] in
-                if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
-                    appDelegate.reloadDcContext()
-                }
-            }
-            self.navigationController?.pushViewController(accountSetupController, animated: true)
+            self.showAccountSetupController()
         }
         view.onScanQRCode  = { [weak self] in
             guard let self = self else { return }
@@ -46,6 +39,19 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
         return UIBarButtonItem(title: String.localized("cancel"), style: .plain, target: self, action: #selector(cancelButtonPressed))
     }()
 
+    private lazy var moreButton: UIBarButtonItem = {
+        let image: UIImage?
+        if #available(iOS 13.0, *) {
+            image = UIImage(systemName: "ellipsis.circle")
+        } else {
+            image = UIImage(named: "ic_more")
+        }
+        return UIBarButtonItem(image: image,
+                               style: .plain,
+                               target: self,
+                               action: #selector(moreButtonPressed))
+    }()
+
     private var qrCodeReader: QrCodeReaderController?
     weak var progressAlert: UIAlertController?
 
@@ -77,6 +83,7 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
         if canCancel {
             navigationItem.leftBarButtonItem = cancelButton
         }
+        navigationItem.rightBarButtonItem = moreButton
     }
 
     override func viewDidLayoutSubviews() {
@@ -128,6 +135,7 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
         if dcAccounts.getSelected().isConfigured() {
             UserDefaults.standard.setValue(dcAccounts.getSelected().id, forKey: Constants.Keys.lastSelectedAccountKey)
 
+            //FIXME: what do we want to do with QR-Code created accounts? For now: adding an unencrypted account
             // ensure we're configuring on an empty new account
             _ = dcAccounts.add()
         }
@@ -162,6 +170,47 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
         present(alert, animated: true)
     }
 
+    @objc private func moreButtonPressed() {
+        let alert = UIAlertController(title: "Encrypted Account (experimental)",
+                                      message: "Do you want to encrypt your account database? This cannot be undone.",
+                                      preferredStyle: .safeActionSheet)
+        let encryptedAccountAction = UIAlertAction(title: "Create encrypted account", style: .default, handler: switchToEncrypted(_:))
+        let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .destructive, handler: nil)
+        alert.addAction(encryptedAccountAction)
+        alert.addAction(cancelAction)
+        self.present(alert, animated: true, completion: nil)
+    }
+
+    private func switchToEncrypted(_ action: UIAlertAction) {
+        let lastContextId = dcAccounts.getSelected().id
+        let newContextId = dcAccounts.addClosedAccount()
+        _ = dcAccounts.remove(id: lastContextId)
+        _ = dcAccounts.select(id: newContextId)
+        let selected = dcAccounts.getSelected()
+        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() {
+        let accountSetupController = AccountSetupController(dcAccounts: self.dcAccounts, editView: false)
+        accountSetupController.onLoginSuccess = {
+            if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
+                appDelegate.reloadDcContext()
+            }
+        }
+        self.navigationController?.pushViewController(accountSetupController, animated: true)
+    }
+
     @objc private func cancelButtonPressed() {
         guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
         // take a bit care on account removal:

+ 4 - 0
deltachat-ios/deltachat-ios.entitlements

@@ -14,5 +14,9 @@
 	<array>
 		<string>group.chat.delta.ios</string>
 	</array>
+	<key>keychain-access-groups</key>
+	<array>
+		<string>$(AppIdentifierPrefix)group.chat.delta.ios</string>
+	</array>
 </dict>
 </plist>