Implemented setting database password in iOS

This commit is contained in:
dankito 2020-10-14 00:18:36 +02:00
parent 5c63af15a0
commit 5acb2353c7
5 changed files with 131 additions and 74 deletions

View File

@ -35,18 +35,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
} }
private func setupBankingUi() -> AuthenticationService { private func setupBankingUi() -> AuthenticationService {
let appDataFolder = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
?? Bundle.main.resourceURL?.absoluteString ?? ""
let persistence = CoreDataBankingPersistence() let persistence = CoreDataBankingPersistence()
let authenticationService = AuthenticationService() let authenticationService = AuthenticationService(persistence)
self.persistence = 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) DependencyInjector.register(dependency: authenticationService)
return authenticationService return authenticationService
@ -60,7 +52,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
} }
private func showApplicationMainView(window: UIWindow) { 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())
}
} }

View File

@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import LocalAuthentication import LocalAuthentication
import BankingUiSwift
class AuthenticationService { class AuthenticationService {
@ -12,13 +13,22 @@ class AuthenticationService {
private let biometricAuthenticationService = BiometricAuthenticationService() private let biometricAuthenticationService = BiometricAuthenticationService()
private let persistence: IBankingPersistence
init() {
init(_ persistence: IBankingPersistence) {
self.persistence = persistence
if let type = readAuthenticationType() { if let type = readAuthenticationType() {
self.authenticationType = type self.authenticationType = type
if type == .none {
openDatabase(false, nil)
}
} }
else { // first app run, no authentication type persisted yet -> set to .unprotected else { // first app run, no authentication type persisted yet -> set to .unprotected
removeAppProtection() 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) { func setAuthenticationMethodToPassword(_ newLoginPassword: String) {
setAuthenticationType(.password) setAuthenticationType(.password)
@ -144,10 +189,10 @@ class AuthenticationService {
if let newLoginPassword = newLoginPassword { if let newLoginPassword = newLoginPassword {
setLoginPassword(newLoginPassword) setLoginPassword(newLoginPassword)
databasePassword = newLoginPassword + "_" + databasePassword databasePassword = concatPasswords(newLoginPassword, databasePassword)
} }
return true return persistence.changePassword(newPassword: map(databasePassword))
} catch { } catch {
NSLog("Could not save default password: \(error)") NSLog("Could not save default password: \(error)")
} }
@ -172,6 +217,18 @@ class AuthenticationService {
return nil 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 { private func createDefaultPasswordKeychainItem(_ useBiometricAuthentication: Bool) -> KeychainPasswordItem {
var accessControl: SecAccessControl? = nil var accessControl: SecAccessControl? = nil
var context: LAContext? = 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 { private func generateRandomPassword(_ passwordLength: Int) -> String {
let dictionary = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789§±!@#$%^&*-_=+;:|/?.>,<" let dictionary = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789§±!@#$%^&*-_=+;:|/?.>,<"
return String((0 ..< passwordLength).map{ _ in dictionary.randomElement()! }) 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
}
} }

View File

@ -10,53 +10,13 @@ class CoreDataBankingPersistence: IBankingPersistence, ITransactionPartySearcher
private let mapper = Mapper() private let mapper = Mapper()
lazy var persistentContainer: NSPersistentContainer = { private 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
}()
lazy var context: NSManagedObjectContext = { lazy var context: NSManagedObjectContext = {
return persistentContainer.viewContext return persistentContainer!.viewContext
}() }()
func saveContext () { func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges { if context.hasChanges {
do { do {
try context.save() try context.save()
@ -71,11 +31,46 @@ class CoreDataBankingPersistence: IBankingPersistence, ITransactionPartySearcher
func decryptData(password: KotlinCharArray) -> Bool { 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 { 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 { do {
let result = try encryptedStore.changeDatabasePassphrase(map(newPassword)) let result = try encryptedStore.changeDatabasePassphrase(map(newPassword))

View File

@ -130,7 +130,7 @@ struct LoginDialog: View {
struct LoginDialog_Previews: PreviewProvider { struct LoginDialog_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
LoginDialog(AuthenticationService()) { _ in } LoginDialog(AuthenticationService(CoreDataBankingPersistence())) { _ in }
} }
} }

View File

@ -43,10 +43,11 @@ struct ProtectAppSettingsDialog: View {
init() { init() {
let currentAuthenticationType = authenticationService.authenticationType let currentAuthenticationType = authenticationService.authenticationType
let isBiometricAuthenticationSupported = authenticationService.deviceSupportsFaceID || authenticationService.deviceSupportsTouchID
var authenticationTypes = [AuthenticationType]() var authenticationTypes = [AuthenticationType]()
if authenticationService.deviceSupportsFaceID || authenticationService.deviceSupportsTouchID { if isBiometricAuthenticationSupported {
authenticationTypes.append(.biometric) authenticationTypes.append(.biometric)
} }
@ -59,7 +60,7 @@ struct ProtectAppSettingsDialog: View {
self.supportedAuthenticationTypes = authenticationTypes self.supportedAuthenticationTypes = authenticationTypes
if currentAuthenticationType == .biometric || currentAuthenticationType != .password { if currentAuthenticationType == .biometric && isBiometricAuthenticationSupported {
if authenticationService.deviceSupportsFaceID { if authenticationService.deviceSupportsFaceID {
_isFaceIDSelected = State(initialValue: true) _isFaceIDSelected = State(initialValue: true)
} }