485 lines
16 KiB
Swift
485 lines
16 KiB
Swift
import SwiftUI
|
|
import LocalAuthentication
|
|
import CryptoSwift
|
|
import BankingUiSwift
|
|
|
|
|
|
class AuthenticationService {
|
|
|
|
static private let AuthenticationTypeKeychainAccountName = "AuthenticationType"
|
|
|
|
static private let DefaultPasswordKeychainAccountName = "DefaultPassword"
|
|
|
|
static private let UserLoginPasswordKeychainAccountName = "UserLoginPassword"
|
|
|
|
static private let UserLoginPasswordSaltKeychainAccountName = "UserLoginPasswordSalt"
|
|
|
|
static private let Key = ">p(Z5&RRA,@_+W0#" // length = 16 // TODO: find a better way to store key
|
|
|
|
static private let IV = "drowssapdrowssap" // length = 16
|
|
|
|
|
|
private let biometricAuthenticationService = BiometricAuthenticationService()
|
|
|
|
private let persistence: IBankingPersistence
|
|
|
|
|
|
init(_ persistence: IBankingPersistence) {
|
|
self.persistence = persistence
|
|
|
|
if UserDefaults.standard.bool(forKey: "hasAppBeenStartedBefore", defaultValue: false) == false { // when uninstalling app key chain items aren't deleted -> delete them after reinstall
|
|
deleteAllKeyChainItems()
|
|
UserDefaults.standard.setValue(true, forKey: "hasAppBeenStartedBefore")
|
|
}
|
|
|
|
if let type = readAuthenticationType() {
|
|
self.authenticationType = type
|
|
|
|
if type == .none {
|
|
openDatabase(false, nil)
|
|
}
|
|
}
|
|
else { // first app run, no authentication type persisted yet -> set default password
|
|
removeAppProtection()
|
|
openDatabase(false, nil)
|
|
}
|
|
}
|
|
|
|
|
|
private (set) var authenticationType: AuthenticationType = .none
|
|
|
|
var needsAuthenticationToUnlockApp: Bool {
|
|
let authenticationType = self.authenticationType
|
|
|
|
return authenticationType != .none
|
|
}
|
|
|
|
var needsBiometricAuthenticationToUnlockApp: Bool {
|
|
let authenticationType = self.authenticationType
|
|
|
|
return authenticationType == .biometric
|
|
}
|
|
|
|
var needsFaceIDToUnlockApp: Bool {
|
|
return self.needsBiometricAuthenticationToUnlockApp && self.deviceSupportsFaceID
|
|
}
|
|
|
|
var needsTouchIDToUnlockApp: Bool {
|
|
return self.needsBiometricAuthenticationToUnlockApp && self.deviceSupportsTouchID
|
|
}
|
|
|
|
var needsPasswordToUnlockApp: Bool {
|
|
return self.authenticationType == .password
|
|
}
|
|
|
|
|
|
var deviceSupportsFaceID: Bool {
|
|
return biometricAuthenticationService.isFaceIDSupported
|
|
}
|
|
|
|
var deviceSupportsTouchID: Bool {
|
|
return biometricAuthenticationService.isTouchIDSupported
|
|
}
|
|
|
|
var supportedBiometricAuthenticationLocalizedName: String {
|
|
if deviceSupportsTouchID {
|
|
return "TouchID".localize()
|
|
}
|
|
else {
|
|
return "FaceID".localize()
|
|
}
|
|
}
|
|
|
|
|
|
func loginUserWithPassword(_ enteredPassword: String, _ authenticationResult: @escaping (Bool, String?) -> Void) {
|
|
if let storedHash = readLoginPasswordHash() {
|
|
if let salt = readLoginPasswordSalt() {
|
|
if let hashOfEnteredPassword = hashLoginPassword(enteredPassword, salt) {
|
|
if storedHash == hashOfEnteredPassword {
|
|
let decryptDatabaseResult = openDatabase(false, enteredPassword)
|
|
authenticationResult(decryptDatabaseResult, nil)
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
authenticationResult(false, "Incorrect password entered".localize())
|
|
}
|
|
|
|
func loginUserWithBiometric(_ prompt: String, _ authenticationResult: @escaping (Bool, String?) -> Void) {
|
|
authenticateUserWithBiometric(prompt) { successful, error in
|
|
var decryptDatabaseResult = false
|
|
if successful {
|
|
decryptDatabaseResult = self.openDatabase(true, nil)
|
|
}
|
|
|
|
authenticationResult(successful && decryptDatabaseResult, error)
|
|
}
|
|
}
|
|
|
|
func authenticateUserWithBiometricToSetAsNewAuthenticationMethod(_ prompt: String, _ authenticationResult: @escaping (Bool, String?) -> Void) {
|
|
authenticateUserWithBiometric(prompt, authenticationResult)
|
|
}
|
|
|
|
private func authenticateUserWithBiometric(_ prompt: String, _ authenticationResult: @escaping (Bool, String?) -> Void) {
|
|
biometricAuthenticationService.authenticate(prompt) { successful, error in
|
|
authenticationResult(successful, error)
|
|
}
|
|
}
|
|
|
|
@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)
|
|
|
|
setPasswords(false, newLoginPassword)
|
|
}
|
|
|
|
func setAuthenticationMethodToBiometric() {
|
|
setAuthenticationType(.biometric)
|
|
|
|
setPasswords(true, nil)
|
|
}
|
|
|
|
func removeAppProtection() {
|
|
setAuthenticationType(.none)
|
|
|
|
setPasswords(false, nil)
|
|
}
|
|
|
|
|
|
private func readAuthenticationType() -> AuthenticationType? {
|
|
do {
|
|
let authenticationTypeItem = createAuthenticationTypeKeychainItem()
|
|
|
|
if let authenticationTypeString = try decrypt(authenticationTypeItem.readPassword()) {
|
|
return AuthenticationType.init(rawValue: authenticationTypeString)
|
|
}
|
|
} catch {
|
|
NSLog("Could not read AuthenticationType: \(error)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func setAuthenticationType(_ type: AuthenticationType) {
|
|
if needsPasswordToUnlockApp {
|
|
deleteLoginPassword()
|
|
}
|
|
|
|
do {
|
|
if let encrypted = encrypt(type.rawValue) {
|
|
let authenticationTypeItem = createAuthenticationTypeKeychainItem()
|
|
|
|
try authenticationTypeItem.savePassword(encrypted)
|
|
}
|
|
} catch {
|
|
NSLog("Could not save AuthenticationType: \(error)")
|
|
}
|
|
|
|
self.authenticationType = type
|
|
}
|
|
|
|
private func deleteAuthenticationTypeKeychainItem() {
|
|
do {
|
|
let item = createAuthenticationTypeKeychainItem()
|
|
|
|
try item.deleteItem()
|
|
} catch {
|
|
NSLog("Could not delete authentication type keychain item: \(error)")
|
|
}
|
|
}
|
|
|
|
private func createAuthenticationTypeKeychainItem() -> KeychainPasswordItem {
|
|
return KeychainPasswordItem(Self.AuthenticationTypeKeychainAccountName)
|
|
}
|
|
|
|
|
|
@discardableResult
|
|
private func setPasswords(_ useBiometricAuthentication: Bool, _ newLoginPassword: String?) -> Bool {
|
|
deleteDefaultPassword(useBiometricAuthentication) // TODO: needed?
|
|
|
|
var databasePassword = ""
|
|
|
|
if let newDefaultPassword = createAndSetDefaultPassword(useBiometricAuthentication) {
|
|
databasePassword = newDefaultPassword
|
|
}
|
|
|
|
if let newLoginPassword = newLoginPassword {
|
|
setLoginPassword(newLoginPassword)
|
|
databasePassword = concatPasswords(newLoginPassword, databasePassword)
|
|
}
|
|
|
|
return persistence.changePassword(newPassword: map(databasePassword))
|
|
}
|
|
|
|
@discardableResult
|
|
private func createAndSetDefaultPassword(_ useBiometricAuthentication: Bool) -> String? {
|
|
do {
|
|
let newDefaultPassword = generateRandomPassword(30)
|
|
|
|
if let encrypedNewDefaultPassword = encrypt(newDefaultPassword) {
|
|
let passwordItem = createDefaultPasswordKeychainItem(useBiometricAuthentication)
|
|
|
|
try passwordItem.savePassword(encrypedNewDefaultPassword)
|
|
|
|
return newDefaultPassword
|
|
}
|
|
} catch {
|
|
NSLog("Could not create new default password: \(error)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func readDefaultPassword(_ useBiometricAuthentication: Bool) -> String? {
|
|
do {
|
|
let passwordItem = createDefaultPasswordKeychainItem(useBiometricAuthentication)
|
|
|
|
return try decrypt(passwordItem.readPassword())
|
|
} catch {
|
|
NSLog("Could not read default password: \(error)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
@discardableResult
|
|
private func deleteDefaultPassword(_ useBiometricAuthentication: Bool) -> Bool {
|
|
do {
|
|
let passwordItem = createDefaultPasswordKeychainItem(useBiometricAuthentication)
|
|
|
|
return deleteDefaultPassword(passwordItem)
|
|
} catch {
|
|
NSLog("Could not delete default password: \(error)")
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
@discardableResult
|
|
private func deleteDefaultPassword(_ passwordItem: KeychainPasswordItem) -> Bool {
|
|
do {
|
|
try? passwordItem.deleteItem()
|
|
|
|
return true
|
|
} catch {
|
|
NSLog("Could not delete default password: \(error)")
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func createDefaultPasswordKeychainItem(_ useBiometricAuthentication: Bool) -> KeychainPasswordItem {
|
|
var accessControl: SecAccessControl? = nil
|
|
var context: LAContext? = nil
|
|
|
|
if useBiometricAuthentication {
|
|
accessControl = SecAccessControlCreateWithFlags(nil, // Use the default allocator.
|
|
kSecAttrAccessibleWhenUnlocked,
|
|
.userPresence,
|
|
nil) // Ignore any error.
|
|
|
|
context = LAContext()
|
|
context?.touchIDAuthenticationAllowableReuseDuration = 45
|
|
}
|
|
|
|
return KeychainPasswordItem(service: Self.DefaultPasswordKeychainAccountName, account: nil, accessGroup: nil, secAccessControl: accessControl, authenticationContext: context)
|
|
}
|
|
|
|
|
|
@discardableResult
|
|
private func setLoginPassword(_ newPassword: String) -> Bool {
|
|
do {
|
|
let salt = Array(generateRandomPassword(8).utf8)
|
|
|
|
if let passwordHash = hashLoginPassword(newPassword, salt) {
|
|
if saveLoginPasswordSalt(salt) {
|
|
let passwordItem = createUserLoginPasswordKeychainItem()
|
|
|
|
try passwordItem.savePassword(passwordHash)
|
|
|
|
return true
|
|
}
|
|
}
|
|
} catch {
|
|
NSLog("Could not save login password: \(error)")
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func readLoginPasswordHash() -> String? {
|
|
do {
|
|
let passwordItem = createUserLoginPasswordKeychainItem()
|
|
|
|
return try passwordItem.readPassword()
|
|
} catch {
|
|
NSLog("Could not read login password: \(error)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
@discardableResult
|
|
private func deleteLoginPassword() -> Bool {
|
|
do {
|
|
let passwordItem = createUserLoginPasswordKeychainItem()
|
|
|
|
try passwordItem.deleteItem()
|
|
|
|
return true
|
|
} catch {
|
|
NSLog("Could not delete login password: \(error)")
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func createUserLoginPasswordKeychainItem() -> KeychainPasswordItem {
|
|
return KeychainPasswordItem(Self.UserLoginPasswordKeychainAccountName)
|
|
}
|
|
|
|
|
|
@discardableResult
|
|
private func saveLoginPasswordSalt(_ salt: Array<UInt8>) -> Bool {
|
|
do {
|
|
let saltItem = createUserLoginPasswordSaltKeychainItem()
|
|
|
|
if let saltBase64Encoded = salt.toBase64() {
|
|
try saltItem.savePassword(saltBase64Encoded)
|
|
|
|
return true
|
|
}
|
|
} catch {
|
|
NSLog("Could not save login password salt: \(error)")
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func readLoginPasswordSalt() -> Array<UInt8>? {
|
|
do {
|
|
let saltItem = createUserLoginPasswordSaltKeychainItem()
|
|
|
|
let saltBase64Encoded = try saltItem.readPassword()
|
|
|
|
return Array<UInt8>(base64: saltBase64Encoded)
|
|
} catch {
|
|
NSLog("Could not read login password salt: \(error)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
@discardableResult
|
|
private func deleteLoginPasswordSalt() -> Bool {
|
|
do {
|
|
let saltItem = createUserLoginPasswordSaltKeychainItem()
|
|
|
|
try saltItem.deleteItem()
|
|
|
|
return true
|
|
} catch {
|
|
NSLog("Could not delete login password salt: \(error)")
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func createUserLoginPasswordSaltKeychainItem() -> KeychainPasswordItem {
|
|
return KeychainPasswordItem(Self.UserLoginPasswordSaltKeychainAccountName)
|
|
}
|
|
|
|
|
|
private func deleteAllKeyChainItems() {
|
|
deleteAuthenticationTypeKeychainItem()
|
|
|
|
deleteDefaultPassword(false) // TODO: which boolean value to set here? does it make any difference if it comes to deleting the key chain item?a
|
|
deleteDefaultPassword(true)
|
|
|
|
deleteLoginPassword()
|
|
|
|
deleteLoginPasswordSalt()
|
|
}
|
|
|
|
|
|
private func generateRandomPassword(_ passwordLength: Int) -> String {
|
|
let dictionary = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789§±!@#$%^&*-_=+;:|/?.>,<"
|
|
|
|
return String((0 ..< passwordLength).map{ _ in dictionary.randomElement()! })
|
|
}
|
|
|
|
|
|
private func encrypt(_ string: String) -> String? {
|
|
do {
|
|
let cipher = try getCipher()
|
|
|
|
let cipherText = try cipher.encrypt(Array(string.utf8))
|
|
|
|
return cipherText.toBase64()
|
|
} catch {
|
|
NSLog("Could not encrypt value: \(error)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func decrypt(_ base64EncodedCipherText: String) -> String? {
|
|
do {
|
|
let bytes = Array<UInt8>(base64: base64EncodedCipherText)
|
|
|
|
let cipher = try getCipher()
|
|
|
|
let decryptedBytes = try cipher.decrypt(bytes)
|
|
|
|
return String(bytes: decryptedBytes, encoding: .utf8)
|
|
} catch {
|
|
NSLog("Could not decrypt cipher text: \(error)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func getCipher() throws -> AES {
|
|
return try AES(key: Self.Key, iv: Self.IV)
|
|
}
|
|
|
|
|
|
private func hashLoginPassword(_ loginPassword: String, _ salt: Array<UInt8>) -> String? {
|
|
do {
|
|
let password = Array(loginPassword.utf8)
|
|
|
|
let bytes = try Scrypt(password: password, salt: salt, dkLen: 64, N: 256, r: 8, p: 1).calculate()
|
|
|
|
return bytes.toBase64()
|
|
} catch {
|
|
NSLog("Could not create hash for login password: \(error)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func concatPasswords(_ loginPassword: String, _ defaultPassword: String) -> String {
|
|
return loginPassword + "_" + defaultPassword
|
|
}
|
|
|
|
private func map(_ string: String) -> KotlinCharArray {
|
|
return string.toKotlinCharArray()
|
|
}
|
|
|
|
}
|