From b1fe9157218d3bee345b4fb18a57f5aacd068997 Mon Sep 17 00:00:00 2001 From: Davide Ramo Date: Wed, 8 Feb 2023 14:36:01 +0100 Subject: [PATCH 01/10] feat: add biometric keychain storage --- ios/Plugin/Plugin.swift | 131 +++++++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 16 deletions(-) diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index 38f79b2..b079071 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -149,12 +149,32 @@ public class NativeBiometric: CAPPlugin { call.reject("No server name was provided") return } - do{ - let credentials = try getCredentialsFromKeychain(server) - var obj = JSObject() - obj["username"] = credentials.username - obj["password"] = credentials.password - call.resolve(obj) + + let reason = call.getString("reason") ?? "For biometric authentication" + + let authContext = LAContext() + + do { + let accessControl = try getBioSecAccessControl() + authContext.evaluateAccessControl(accessControl, + operation: .useItem, + localizedReason: reason) { [weak self] (laSuccess, laError) in + + do{ + if laSuccess, let credentials = try self?.getCredentialsFromKeychain(server, + context: authContext) { + var obj = JSObject() + obj["username"] = credentials.username + obj["password"] = credentials.password + call.resolve(obj) + } else { + call.reject(laError?.localizedDescription ?? "Biometric error") + } + } catch { + call.reject(error.localizedDescription) + } + + } } catch { call.reject(error.localizedDescription) } @@ -162,15 +182,19 @@ public class NativeBiometric: CAPPlugin { @objc func setCredentials(_ call: CAPPluginCall){ - guard let server = call.getString("server"), let username = call.getString("username"), let password = call.getString("password") else { + guard let server = call.getString("server"), + let username = call.getString("username"), + let password = call.getString("password") else { call.reject("Missing properties") return; } + let reason = call.getString("reason") ?? "For biometric authentication" + let credentials = Credentials(username: username, password: password) do{ - try storeCredentialsInKeychain(credentials, server) + try storeCredentialsInKeychain(credentials, server, prompt:reason) call.resolve() } catch KeychainError.duplicateItem { do { @@ -198,14 +222,52 @@ public class NativeBiometric: CAPPlugin { } } + private func getBioSecAccessControl() throws -> SecAccessControl { + var error: Unmanaged? + + let flags: SecAccessControlCreateFlags + if #available(iOS 11.3, *) { + flags = [.privateKeyUsage, .biometryCurrentSet] + } else { + flags = [.privateKeyUsage, .touchIDCurrentSet] + } + + guard let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + flags, + &error) else { + throw KeychainError.unhandledError(status: 0) + } + + return access + } // Store user Credentials in Keychain - func storeCredentialsInKeychain(_ credentials: Credentials, _ server: String) throws { - let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + func storeCredentialsInKeychain(_ credentials: Credentials, + _ server: String, + context: LAContext? = nil, + prompt: String? = nil) throws { + + let acl = try getBioSecAccessControl() + var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, kSecAttrAccount as String: credentials.username, kSecAttrServer as String: server, + kSecAttrAccessControl as String: acl, kSecValueData as String: credentials.password.data(using: .utf8)!] + if let context = context { + query[kSecUseAuthenticationContext as String] = context + + // Prevent system UI from automatically requesting Touc ID/Face ID authentication + // just in case someone passes here an LAContext instance without + // a prior evaluateAccessControl call + query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUISkip + } + + if let prompt = prompt { + query[kSecUseOperationPrompt as String] = prompt + } + let status = SecItemAdd(query as CFDictionary, nil) guard status != errSecDuplicateItem else { throw KeychainError.duplicateItem } @@ -213,10 +275,29 @@ public class NativeBiometric: CAPPlugin { } // Update user Credentials in Keychain - func updateCredentialsInKeychain(_ credentials: Credentials, _ server: String) throws{ - let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + func updateCredentialsInKeychain(_ credentials: Credentials, + _ server: String, + context: LAContext? = nil, + prompt: String? = nil) throws { + + let acl = try getBioSecAccessControl() + var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrAccessControl as String: acl, kSecAttrServer as String: server] + if let context = context { + query[kSecUseAuthenticationContext as String] = context + + // Prevent system UI from automatically requesting Touc ID/Face ID authentication + // just in case someone passes here an LAContext instance without + // a prior evaluateAccessControl call + query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUISkip + } + + if let prompt = prompt { + query[kSecUseOperationPrompt as String] = prompt + } + let account = credentials.username let password = credentials.password.data(using: String.Encoding.utf8)! let attributes: [String: Any] = [kSecAttrAccount as String: account, @@ -228,13 +309,30 @@ public class NativeBiometric: CAPPlugin { } // Get user Credentials from Keychain - func getCredentialsFromKeychain(_ server: String) throws -> Credentials { - let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + func getCredentialsFromKeychain(_ server: String, + context: LAContext? = nil, + prompt: String? = nil) throws -> Credentials { + let acl = try getBioSecAccessControl() + var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, kSecAttrServer as String: server, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnAttributes as String: true, + kSecAttrAccessControl as String: acl, kSecReturnData as String: true] + if let context = context { + query[kSecUseAuthenticationContext as String] = context + + // Prevent system UI from automatically requesting Touc ID/Face ID authentication + // just in case someone passes here an LAContext instance without + // a prior evaluateAccessControl call + query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUISkip + } + + if let prompt = prompt { + query[kSecUseOperationPrompt as String] = prompt + } + var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) guard status != errSecItemNotFound else { throw KeychainError.noPassword } @@ -255,8 +353,9 @@ public class NativeBiometric: CAPPlugin { } // Delete user Credentials from Keychain - func deleteCredentialsFromKeychain(_ server: String)throws{ - let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + func deleteCredentialsFromKeychain(_ server: String) throws { + + var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, kSecAttrServer as String: server] let status = SecItemDelete(query as CFDictionary) From 155a214dbd03f66b6bb4009e1014ef2e895d3b4d Mon Sep 17 00:00:00 2001 From: Davide Ramo Date: Thu, 9 Feb 2023 14:51:13 +0100 Subject: [PATCH 02/10] feat: biometric keystore android --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 7 +- android/android.iml | 5 - android/local.properties | 8 - .../epicshaggy/biometric/AuthActivity.java | 206 +++++++-- .../com/epicshaggy/biometric/Credentials.java | 13 + .../epicshaggy/biometric/CryptoManager.java | 217 +++++++++ .../epicshaggy/biometric/NativeBiometric.java | 425 ++++++++---------- 8 files changed, 576 insertions(+), 305 deletions(-) delete mode 100644 .DS_Store delete mode 100644 android/local.properties create mode 100644 android/src/main/java/com/epicshaggy/biometric/Credentials.java create mode 100644 android/src/main/java/com/epicshaggy/biometric/CryptoManager.java diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 33e63007153517a78ecda095d15f29bc4d50f38a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOHRWu5FNLX_!U%kY>;vRB5{LI%7P6W4gjyw4@^=2S8(elr*L6x+uq^qNS|^ z%78MkZ4B_YyF(8&r;HZleZS=hGrTQapd{Z`&?1WI68bMFV|?uuY!-BbUWMK(<{WDmV$Yq9Xp0K)-=FQ0TiO9Z(Uf^MaojDEc_P|<>sF~$3 zK`lFKgE2o+-#)nlD_kLFf>mO5^ay??Zne#o8fMbB?;@W?_*^=?gMWC89O8K{+rHN@ zfj7n+d6sabvV{!b%x2l;JwQbnPzIC%p8?(hWZ|FKmklRDH|8BhkA40K($ z&*%Tu=KFusNPEhFGVre$FwyXKI6z8rwziVvvo=P#M`2;T+MzW;CC9OC@KJn%Vg++C WH-M>N?GPgn{SmM