From 5acb2353c7acb8ccb1749639a4e3350b9427eecb Mon Sep 17 00:00:00 2001 From: dankito Date: Wed, 14 Oct 2020 00:18:36 +0200 Subject: [PATCH] Implemented setting database password in iOS --- .../BankingiOSApp/SceneDelegate.swift | 23 +++-- .../Security/AuthenticationService.swift | 92 +++++++++++++++---- .../CoreDataBankingPersistence.swift | 83 ++++++++--------- .../ui/dialogs/LoginDialog.swift | 2 +- .../ui/dialogs/ProtectAppSettingsDialog.swift | 5 +- 5 files changed, 131 insertions(+), 74 deletions(-) diff --git a/ui/BankingiOSApp/BankingiOSApp/SceneDelegate.swift b/ui/BankingiOSApp/BankingiOSApp/SceneDelegate.swift index ab863982..a01aef80 100644 --- a/ui/BankingiOSApp/BankingiOSApp/SceneDelegate.swift +++ b/ui/BankingiOSApp/BankingiOSApp/SceneDelegate.swift @@ -35,18 +35,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func setupBankingUi() -> AuthenticationService { - let appDataFolder = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first - ?? Bundle.main.resourceURL?.absoluteString ?? "" - let persistence = CoreDataBankingPersistence() - let authenticationService = AuthenticationService() + let authenticationService = AuthenticationService(persistence) self.persistence = persistence - let dataFolder = URL(fileURLWithPath: "data", isDirectory: true, relativeTo: URL(fileURLWithPath: appDataFolder)) - - let presenter = BankingPresenterSwift(dataFolder: dataFolder, router: SwiftUiRouter(), webClient: UrlSessionWebClient(), persistence: persistence, transactionPartySearcher: persistence, bankIconFinder: SwiftBankIconFinder(), serializer: NoOpSerializer(), asyncRunner: DispatchQueueAsyncRunner()) - - DependencyInjector.register(dependency: presenter) DependencyInjector.register(dependency: authenticationService) return authenticationService @@ -60,7 +52,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func showApplicationMainView(window: UIWindow) { - window.rootViewController = UINavigationController(rootViewController: TabBarController()) + if let persistence = persistence { + let appDataFolder = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first + ?? Bundle.main.resourceURL?.absoluteString ?? "" + + let dataFolder = URL(fileURLWithPath: "data", isDirectory: true, relativeTo: URL(fileURLWithPath: appDataFolder)) + + let presenter = BankingPresenterSwift(dataFolder: dataFolder, router: SwiftUiRouter(), webClient: UrlSessionWebClient(), persistence: persistence, transactionPartySearcher: persistence, bankIconFinder: SwiftBankIconFinder(), serializer: NoOpSerializer(), asyncRunner: DispatchQueueAsyncRunner()) + + DependencyInjector.register(dependency: presenter) + + window.rootViewController = UINavigationController(rootViewController: TabBarController()) + } } diff --git a/ui/BankingiOSApp/BankingiOSApp/Security/AuthenticationService.swift b/ui/BankingiOSApp/BankingiOSApp/Security/AuthenticationService.swift index 4d6c5274..7b34aa00 100644 --- a/ui/BankingiOSApp/BankingiOSApp/Security/AuthenticationService.swift +++ b/ui/BankingiOSApp/BankingiOSApp/Security/AuthenticationService.swift @@ -1,5 +1,6 @@ import SwiftUI import LocalAuthentication +import BankingUiSwift class AuthenticationService { @@ -12,13 +13,22 @@ class AuthenticationService { private let biometricAuthenticationService = BiometricAuthenticationService() + private let persistence: IBankingPersistence - init() { + + init(_ persistence: IBankingPersistence) { + self.persistence = persistence + if let type = readAuthenticationType() { self.authenticationType = type + + if type == .none { + openDatabase(false, nil) + } } else { // first app run, no authentication type persisted yet -> set to .unprotected removeAppProtection() + openDatabase(false, nil) } } @@ -68,6 +78,41 @@ class AuthenticationService { } + func authenticateUserWithBiometric(_ prompt: String, _ authenticationResult: @escaping (Bool, String?) -> Void) { + biometricAuthenticationService.authenticate(prompt) { successful, error in + var decryptDatabaseResult = false + if successful { + decryptDatabaseResult = self.openDatabase(true, nil) + } + + authenticationResult(successful && decryptDatabaseResult, error) + } + } + + func authenticateUserWithPassword(_ enteredPassword: String, _ authenticationResult: @escaping (Bool, String?) -> Void) { + if retrieveLoginPassword() == enteredPassword { + let decryptDatabaseResult = openDatabase(false, enteredPassword) + authenticationResult(decryptDatabaseResult, nil) + } + else { + authenticationResult(false, "Incorrect password entered".localize()) + } + } + + @discardableResult + private func openDatabase(_ useBiometricAuthentication: Bool, _ userLoginPassword: String?) -> Bool { + if var databasePassword = readDefaultPassword(useBiometricAuthentication) { + if let loginPassword = userLoginPassword { + databasePassword = concatPasswords(loginPassword, databasePassword) + } + + return persistence.decryptData(password: map(databasePassword)) + } + + return false + } + + func setAuthenticationMethodToPassword(_ newLoginPassword: String) { setAuthenticationType(.password) @@ -144,10 +189,10 @@ class AuthenticationService { if let newLoginPassword = newLoginPassword { setLoginPassword(newLoginPassword) - databasePassword = newLoginPassword + "_" + databasePassword + databasePassword = concatPasswords(newLoginPassword, databasePassword) } - return true + return persistence.changePassword(newPassword: map(databasePassword)) } catch { NSLog("Could not save default password: \(error)") } @@ -172,6 +217,18 @@ class AuthenticationService { return nil } + private func readDefaultPassword(_ useBiometricAuthentication: Bool) -> String? { + do { + let passwordItem = createDefaultPasswordKeychainItem(useBiometricAuthentication) + + return try passwordItem.readPassword() + } catch { + NSLog("Could not read default password: \(error)") + } + + return nil + } + private func createDefaultPasswordKeychainItem(_ useBiometricAuthentication: Bool) -> KeychainPasswordItem { var accessControl: SecAccessControl? = nil var context: LAContext? = nil @@ -237,24 +294,25 @@ class AuthenticationService { } - func authenticateUserWithBiometric(_ prompt: String, _ authenticationResult: @escaping (Bool, String?) -> Void) { - biometricAuthenticationService.authenticate(prompt, authenticationResult) - } - - func authenticateUserWithPassword(_ enteredPassword: String, _ authenticationResult: @escaping (Bool, String?) -> Void) { - if retrieveLoginPassword() == enteredPassword { - authenticationResult(true, nil) - } - else { - authenticationResult(false, "Incorrect password entered".localize()) - } - } - - private func generateRandomPassword(_ passwordLength: Int) -> String { let dictionary = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789§±!@#$%^&*-_=+;:|/?.>,<" return String((0 ..< passwordLength).map{ _ in dictionary.randomElement()! }) } + + private func concatPasswords(_ loginPassword: String, _ defaultPassword: String) -> String { + return loginPassword + "_" + defaultPassword + } + + private func map(_ string: String) -> KotlinCharArray { + let array = KotlinCharArray(size: Int32(string.count)) + + for i in 0 ..< string.count { + array.set(index: Int32(i), value: (string as NSString).character(at: i)) + } + + return array + } + } diff --git a/ui/BankingiOSApp/BankingiOSApp/persistence/CoreDataBankingPersistence.swift b/ui/BankingiOSApp/BankingiOSApp/persistence/CoreDataBankingPersistence.swift index d58532ec..6155759c 100644 --- a/ui/BankingiOSApp/BankingiOSApp/persistence/CoreDataBankingPersistence.swift +++ b/ui/BankingiOSApp/BankingiOSApp/persistence/CoreDataBankingPersistence.swift @@ -10,53 +10,13 @@ class CoreDataBankingPersistence: IBankingPersistence, ITransactionPartySearcher private let mapper = Mapper() - lazy var persistentContainer: NSPersistentContainer = { - /* - The persistent container for the application. This implementation - creates and returns a container, having loaded the store for the - application to it. This property is optional since there are legitimate - error conditions that could cause the creation of the store to fail. - */ - let container = NSPersistentContainer(name: "BankingiOSApp") - - do { - let options = [ - EncryptedStorePassphraseKey : "someKey" - ] - - let description = try EncryptedStore.makeDescription(options: options, configuration: nil) - container.persistentStoreDescriptions = [ description ] - } - catch { - NSLog("Could not initialize encrypted database storage: " + error.localizedDescription) - } - - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - - return container - }() + private var persistentContainer: NSPersistentContainer? lazy var context: NSManagedObjectContext = { - return persistentContainer.viewContext + return persistentContainer!.viewContext }() func saveContext () { - let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() @@ -71,11 +31,46 @@ class CoreDataBankingPersistence: IBankingPersistence, ITransactionPartySearcher func decryptData(password: KotlinCharArray) -> Bool { - return true + do { + let container = NSPersistentContainer(name: "BankingiOSApp") + + let options = [ + EncryptedStorePassphraseKey : map(password) + ] + + let description = try EncryptedStore.makeDescription(options: options, configuration: nil) + container.persistentStoreDescriptions = [ description ] + + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + + self.persistentContainer = container + + return true + } + catch { + NSLog("Could not initialize encrypted database storage: " + error.localizedDescription) + } + + return false } func changePassword(newPassword: KotlinCharArray) -> Bool { - if let encryptedStore = persistentContainer.persistentStoreCoordinator.persistentStores.first { $0 is EncryptedStore } as? EncryptedStore { + if let encryptedStore = persistentContainer?.persistentStoreCoordinator.persistentStores.first { $0 is EncryptedStore } as? EncryptedStore { do { let result = try encryptedStore.changeDatabasePassphrase(map(newPassword)) diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/LoginDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/LoginDialog.swift index 64f16eae..f4777f9c 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/LoginDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/LoginDialog.swift @@ -130,7 +130,7 @@ struct LoginDialog: View { struct LoginDialog_Previews: PreviewProvider { static var previews: some View { - LoginDialog(AuthenticationService()) { _ in } + LoginDialog(AuthenticationService(CoreDataBankingPersistence())) { _ in } } } diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/ProtectAppSettingsDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/ProtectAppSettingsDialog.swift index 465c13df..c829ec7a 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/ProtectAppSettingsDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/ProtectAppSettingsDialog.swift @@ -43,10 +43,11 @@ struct ProtectAppSettingsDialog: View { init() { let currentAuthenticationType = authenticationService.authenticationType + let isBiometricAuthenticationSupported = authenticationService.deviceSupportsFaceID || authenticationService.deviceSupportsTouchID var authenticationTypes = [AuthenticationType]() - if authenticationService.deviceSupportsFaceID || authenticationService.deviceSupportsTouchID { + if isBiometricAuthenticationSupported { authenticationTypes.append(.biometric) } @@ -59,7 +60,7 @@ struct ProtectAppSettingsDialog: View { self.supportedAuthenticationTypes = authenticationTypes - if currentAuthenticationType == .biometric || currentAuthenticationType != .password { + if currentAuthenticationType == .biometric && isBiometricAuthenticationSupported { if authenticationService.deviceSupportsFaceID { _isFaceIDSelected = State(initialValue: true) }