@capgo/capacitor-native-biometric 8.3.6 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@ import LocalAuthentication
11
11
 
12
12
  @objc(NativeBiometricPlugin)
13
13
  public class NativeBiometricPlugin: CAPPlugin, CAPBridgedPlugin {
14
- private let pluginVersion: String = "8.3.6"
14
+ private let pluginVersion: String = "8.4.0"
15
15
  public let identifier = "NativeBiometricPlugin"
16
16
  public let jsName = "NativeBiometric"
17
17
  public let pluginMethods: [CAPPluginMethod] = [
@@ -20,6 +20,7 @@ public class NativeBiometricPlugin: CAPPlugin, CAPBridgedPlugin {
20
20
  CAPPluginMethod(name: "getCredentials", returnType: CAPPluginReturnPromise),
21
21
  CAPPluginMethod(name: "setCredentials", returnType: CAPPluginReturnPromise),
22
22
  CAPPluginMethod(name: "deleteCredentials", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "getSecureCredentials", returnType: CAPPluginReturnPromise),
23
24
  CAPPluginMethod(name: "isCredentialsSaved", returnType: CAPPluginReturnPromise),
24
25
  CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
25
26
  ]
@@ -190,20 +191,95 @@ public class NativeBiometricPlugin: CAPPlugin, CAPBridgedPlugin {
190
191
  return
191
192
  }
192
193
 
194
+ let accessControl = call.getInt("accessControl") ?? 0
193
195
  let credentials = Credentials(username: username, password: password)
194
196
 
195
- do {
196
- try storeCredentialsInKeychain(credentials, server)
197
- call.resolve()
198
- } catch KeychainError.duplicateItem {
197
+ if accessControl > 0 {
199
198
  do {
200
- try updateCredentialsInKeychain(credentials, server)
199
+ try storeProtectedCredentials(credentials, server, accessControl)
201
200
  call.resolve()
201
+ } catch KeychainError.duplicateItem {
202
+ do {
203
+ try deleteProtectedCredentials(server)
204
+ try storeProtectedCredentials(credentials, server, accessControl)
205
+ call.resolve()
206
+ } catch {
207
+ call.reject(error.localizedDescription)
208
+ }
209
+ } catch {
210
+ call.reject(error.localizedDescription)
211
+ }
212
+ } else {
213
+ do {
214
+ try storeCredentialsInKeychain(credentials, server)
215
+ call.resolve()
216
+ } catch KeychainError.duplicateItem {
217
+ do {
218
+ try updateCredentialsInKeychain(credentials, server)
219
+ call.resolve()
220
+ } catch {
221
+ call.reject(error.localizedDescription)
222
+ }
202
223
  } catch {
203
224
  call.reject(error.localizedDescription)
204
225
  }
205
- } catch {
206
- call.reject(error.localizedDescription)
226
+ }
227
+ }
228
+
229
+ @objc func getSecureCredentials(_ call: CAPPluginCall) {
230
+ guard let server = call.getString("server") else {
231
+ call.reject("No server name was provided")
232
+ return
233
+ }
234
+
235
+ let context = LAContext()
236
+ if let reason = call.getString("reason") {
237
+ context.localizedReason = reason
238
+ }
239
+
240
+ let query: [String: Any] = [
241
+ kSecClass as String: kSecClassGenericPassword,
242
+ kSecAttrService as String: server,
243
+ kSecMatchLimit as String: kSecMatchLimitOne,
244
+ kSecReturnAttributes as String: true,
245
+ kSecReturnData as String: true,
246
+ kSecUseAuthenticationContext as String: context
247
+ ]
248
+
249
+ DispatchQueue.global(qos: .userInitiated).async {
250
+ var item: CFTypeRef?
251
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
252
+
253
+ DispatchQueue.main.async {
254
+ if status == errSecUserCanceled {
255
+ call.reject("User canceled biometric authentication", "16")
256
+ return
257
+ }
258
+ guard status == errSecSuccess else {
259
+ if status == errSecItemNotFound {
260
+ call.reject("No protected credentials found for server", "21")
261
+ } else if status == errSecAuthFailed {
262
+ call.reject("Biometric authentication failed", "10")
263
+ } else {
264
+ call.reject("Failed to retrieve credentials: \(status)", "0")
265
+ }
266
+ return
267
+ }
268
+
269
+ guard let existingItem = item as? [String: Any],
270
+ let passwordData = existingItem[kSecValueData as String] as? Data,
271
+ let password = String(data: passwordData, encoding: .utf8),
272
+ let username = existingItem[kSecAttrAccount as String] as? String
273
+ else {
274
+ call.reject("Unexpected credential data")
275
+ return
276
+ }
277
+
278
+ var obj = JSObject()
279
+ obj["username"] = username
280
+ obj["password"] = password
281
+ call.resolve(obj)
282
+ }
207
283
  }
208
284
  }
209
285
 
@@ -215,6 +291,7 @@ public class NativeBiometricPlugin: CAPPlugin, CAPBridgedPlugin {
215
291
 
216
292
  do {
217
293
  try deleteCredentialsFromKeychain(server)
294
+ try deleteProtectedCredentials(server)
218
295
  call.resolve()
219
296
  } catch {
220
297
  call.reject(error.localizedDescription)
@@ -228,11 +305,10 @@ public class NativeBiometricPlugin: CAPPlugin, CAPBridgedPlugin {
228
305
  }
229
306
 
230
307
  var obj = JSObject()
231
- obj["isSaved"] = checkCredentialsExist(server)
308
+ obj["isSaved"] = checkCredentialsExist(server) || checkProtectedCredentialsExist(server)
232
309
  call.resolve(obj)
233
310
  }
234
311
 
235
- // Check if credentials exist in Keychain
236
312
  func checkCredentialsExist(_ server: String) -> Bool {
237
313
  let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
238
314
  kSecAttrServer as String: server,
@@ -242,7 +318,53 @@ public class NativeBiometricPlugin: CAPPlugin, CAPBridgedPlugin {
242
318
  return status == errSecSuccess
243
319
  }
244
320
 
245
- // Store user Credentials in Keychain
321
+ func checkProtectedCredentialsExist(_ server: String) -> Bool {
322
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
323
+ kSecAttrService as String: server,
324
+ kSecMatchLimit as String: kSecMatchLimitOne]
325
+
326
+ let status = SecItemCopyMatching(query as CFDictionary, nil)
327
+ return status == errSecSuccess
328
+ }
329
+
330
+ func storeProtectedCredentials(_ credentials: Credentials, _ server: String, _ accessControl: Int) throws {
331
+ guard let passwordData = credentials.password.data(using: .utf8) else {
332
+ throw KeychainError.unexpectedPasswordData
333
+ }
334
+
335
+ let flags: SecAccessControlCreateFlags = accessControl == 1 ? .biometryCurrentSet : .biometryAny
336
+ guard let access = SecAccessControlCreateWithFlags(
337
+ kCFAllocatorDefault,
338
+ kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
339
+ flags,
340
+ nil
341
+ ) else {
342
+ throw KeychainError.unhandledError(status: errSecParam)
343
+ }
344
+
345
+ let query: [String: Any] = [
346
+ kSecClass as String: kSecClassGenericPassword,
347
+ kSecAttrService as String: server,
348
+ kSecAttrAccount as String: credentials.username,
349
+ kSecValueData as String: passwordData,
350
+ kSecAttrAccessControl as String: access
351
+ ]
352
+
353
+ let status = SecItemAdd(query as CFDictionary, nil)
354
+ guard status != errSecDuplicateItem else { throw KeychainError.duplicateItem }
355
+ guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
356
+ }
357
+
358
+ func deleteProtectedCredentials(_ server: String) throws {
359
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
360
+ kSecAttrService as String: server]
361
+
362
+ let status = SecItemDelete(query as CFDictionary)
363
+ guard status == errSecSuccess || status == errSecItemNotFound else {
364
+ throw KeychainError.unhandledError(status: status)
365
+ }
366
+ }
367
+
246
368
  func storeCredentialsInKeychain(_ credentials: Credentials, _ server: String) throws {
247
369
  guard let passwordData = credentials.password.data(using: .utf8) else {
248
370
  throw KeychainError.unexpectedPasswordData
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-native-biometric",
3
- "version": "8.3.6",
3
+ "version": "8.4.0",
4
4
  "description": "This plugin gives access to the native biometric apis for android and iOS",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -47,7 +47,8 @@
47
47
  "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
48
48
  "clean": "rimraf ./dist",
49
49
  "watch": "tsc --watch",
50
- "prepublishOnly": "npm run build"
50
+ "prepublishOnly": "npm run build",
51
+ "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@capacitor/android": "^8.0.0",