@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.
package/README.md CHANGED
@@ -283,6 +283,7 @@ This is a plugin specific list of error codes that can be thrown on verifyIdenti
283
283
  * [`getCredentials(...)`](#getcredentials)
284
284
  * [`setCredentials(...)`](#setcredentials)
285
285
  * [`deleteCredentials(...)`](#deletecredentials)
286
+ * [`getSecureCredentials(...)`](#getsecurecredentials)
286
287
  * [`isCredentialsSaved(...)`](#iscredentialssaved)
287
288
  * [`getPluginVersion()`](#getpluginversion)
288
289
  * [Interfaces](#interfaces)
@@ -405,6 +406,29 @@ Deletes the stored credentials for a given server.
405
406
  --------------------
406
407
 
407
408
 
409
+ ### getSecureCredentials(...)
410
+
411
+ ```typescript
412
+ getSecureCredentials(options: GetSecureCredentialsOptions) => Promise<Credentials>
413
+ ```
414
+
415
+ Gets the stored credentials for a given server, requiring biometric authentication.
416
+ Credentials must have been stored with accessControl set to BIOMETRY_CURRENT_SET or BIOMETRY_ANY.
417
+
418
+ On iOS, the system automatically shows the biometric prompt when accessing the protected Keychain item.
419
+ On Android, BiometricPrompt is shown with a CryptoObject bound to the credential decryption key.
420
+
421
+ | Param | Type |
422
+ | ------------- | ----------------------------------------------------------------------------------- |
423
+ | **`options`** | <code><a href="#getsecurecredentialsoptions">GetSecureCredentialsOptions</a></code> |
424
+
425
+ **Returns:** <code>Promise&lt;<a href="#credentials">Credentials</a>&gt;</code>
426
+
427
+ **Since:** 8.4.0
428
+
429
+ --------------------
430
+
431
+
408
432
  ### isCredentialsSaved(...)
409
433
 
410
434
  ```typescript
@@ -502,11 +526,12 @@ Result from isAvailable() method indicating biometric authentication availabilit
502
526
 
503
527
  #### SetCredentialOptions
504
528
 
505
- | Prop | Type |
506
- | -------------- | ------------------- |
507
- | **`username`** | <code>string</code> |
508
- | **`password`** | <code>string</code> |
509
- | **`server`** | <code>string</code> |
529
+ | Prop | Type | Description | Default | Since |
530
+ | ------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ----- |
531
+ | **`username`** | <code>string</code> | | | |
532
+ | **`password`** | <code>string</code> | | | |
533
+ | **`server`** | <code>string</code> | | | |
534
+ | **`accessControl`** | <code><a href="#accesscontrol">AccessControl</a></code> | Access control level for the stored credentials. When set to BIOMETRY_CURRENT_SET or BIOMETRY_ANY, the credentials are hardware-protected and require biometric authentication to access. On iOS, this adds SecAccessControl to the Keychain item. On Android, this creates a biometric-protected Keystore key and requires BiometricPrompt authentication for both storing and retrieving credentials. | <code>AccessControl.NONE</code> | 8.4.0 |
510
535
 
511
536
 
512
537
  #### DeleteCredentialOptions
@@ -516,6 +541,18 @@ Result from isAvailable() method indicating biometric authentication availabilit
516
541
  | **`server`** | <code>string</code> |
517
542
 
518
543
 
544
+ #### GetSecureCredentialsOptions
545
+
546
+ | Prop | Type | Description |
547
+ | ------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------- |
548
+ | **`server`** | <code>string</code> | |
549
+ | **`reason`** | <code>string</code> | Reason for requesting biometric authentication. Displayed in the biometric prompt on both iOS and Android. |
550
+ | **`title`** | <code>string</code> | Title for the biometric prompt. Only for Android. |
551
+ | **`subtitle`** | <code>string</code> | Subtitle for the biometric prompt. Only for Android. |
552
+ | **`description`** | <code>string</code> | Description for the biometric prompt. Only for Android. |
553
+ | **`negativeButtonText`** | <code>string</code> | Text for the negative/cancel button. Only for Android. |
554
+
555
+
519
556
  #### IsCredentialsSavedResult
520
557
 
521
558
  | Prop | Type |
@@ -584,6 +621,15 @@ Callback type for biometry change listener
584
621
  | **`USER_CANCEL`** | <code>16</code> | User canceled the authentication Platform: Android, iOS |
585
622
  | **`USER_FALLBACK`** | <code>17</code> | User chose to use fallback authentication method Platform: Android, iOS |
586
623
 
624
+
625
+ #### AccessControl
626
+
627
+ | Members | Value | Description |
628
+ | -------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
629
+ | **`NONE`** | <code>0</code> | No biometric protection. <a href="#credentials">Credentials</a> are accessible without authentication. This is the default behavior for backward compatibility. |
630
+ | **`BIOMETRY_CURRENT_SET`** | <code>1</code> | Biometric authentication required for credential access. Credentials are invalidated if biometrics change (e.g., new fingerprint enrolled). More secure but credentials are lost if user modifies their biometric enrollment. |
631
+ | **`BIOMETRY_ANY`** | <code>2</code> | Biometric authentication required for credential access. Credentials survive new biometric enrollment (e.g., adding a new fingerprint). More lenient — recommended for most apps. |
632
+
587
633
  </docgen-api>
588
634
  ## Face ID (iOS)
589
635
 
@@ -1,18 +1,21 @@
1
1
  package ee.forgr.biometric;
2
2
 
3
3
  import android.content.Intent;
4
+ import android.content.SharedPreferences;
4
5
  import android.os.Build;
5
6
  import android.os.Bundle;
6
7
  import android.os.Handler;
7
8
  import android.security.keystore.KeyGenParameterSpec;
8
9
  import android.security.keystore.KeyPermanentlyInvalidatedException;
9
10
  import android.security.keystore.KeyProperties;
11
+ import android.util.Base64;
10
12
  import androidx.annotation.NonNull;
11
13
  import androidx.appcompat.app.AppCompatActivity;
12
14
  import androidx.biometric.BiometricManager;
13
15
  import androidx.biometric.BiometricPrompt;
14
16
  import ee.forgr.biometric.capacitornativebiometric.R;
15
17
  import java.io.IOException;
18
+ import java.nio.charset.StandardCharsets;
16
19
  import java.security.GeneralSecurityException;
17
20
  import java.security.KeyStore;
18
21
  import java.security.KeyStoreException;
@@ -24,14 +27,20 @@ import java.util.concurrent.Executor;
24
27
  import javax.crypto.Cipher;
25
28
  import javax.crypto.KeyGenerator;
26
29
  import javax.crypto.SecretKey;
30
+ import javax.crypto.spec.GCMParameterSpec;
31
+ import org.json.JSONObject;
27
32
 
28
33
  public class AuthActivity extends AppCompatActivity {
29
34
 
30
35
  private static final String AUTH_KEY_ALIAS = "NativeBiometricAuthKey";
31
36
  private static final String AUTH_TRANSFORMATION = "AES/GCM/NoPadding";
37
+ private static final String SECURE_KEY_PREFIX = "NativeBiometricSecure_";
38
+ private static final int CREDENTIAL_GCM_IV_LENGTH = 12;
39
+ private static final String SHARED_PREFS_NAME = "NativeBiometricSharedPreferences";
32
40
 
33
41
  private BiometricPrompt biometricPrompt;
34
42
  private Cipher authCipher;
43
+ private String mode;
35
44
  private int maxAttempts;
36
45
  private int counter = 0;
37
46
 
@@ -40,7 +49,9 @@ public class AuthActivity extends AppCompatActivity {
40
49
  super.onCreate(savedInstanceState);
41
50
  setContentView(R.layout.activity_auth_acitivy);
42
51
 
43
- // Get maxAttempts with validation: must be between 1 and 5, default to 1
52
+ mode = getIntent().getStringExtra("mode");
53
+ if (mode == null) mode = "verify";
54
+
44
55
  int rawMaxAttempts = getIntent().getIntExtra("maxAttempts", 1);
45
56
  maxAttempts = Math.max(1, Math.min(5, rawMaxAttempts));
46
57
 
@@ -100,11 +111,17 @@ public class AuthActivity extends AppCompatActivity {
100
111
  @Override
101
112
  public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
102
113
  super.onAuthenticationSucceeded(result);
103
- if (!validateCryptoObject(result)) {
104
- finishActivity("error", 10, "Biometric security check failed");
105
- return;
114
+ if ("setSecureCredentials".equals(mode)) {
115
+ handleSetSecureCredentials(result);
116
+ } else if ("getSecureCredentials".equals(mode)) {
117
+ handleGetSecureCredentials(result);
118
+ } else {
119
+ if (!validateCryptoObject(result)) {
120
+ finishActivity("error", 10, "Biometric security check failed");
121
+ return;
122
+ }
123
+ finishActivity();
106
124
  }
107
- finishActivity();
108
125
  }
109
126
 
110
127
  @Override
@@ -120,7 +137,14 @@ public class AuthActivity extends AppCompatActivity {
120
137
  }
121
138
  );
122
139
 
123
- BiometricPrompt.CryptoObject cryptoObject = createCryptoObject();
140
+ BiometricPrompt.CryptoObject cryptoObject;
141
+ if ("setSecureCredentials".equals(mode)) {
142
+ cryptoObject = createCredentialEncryptCryptoObject();
143
+ } else if ("getSecureCredentials".equals(mode)) {
144
+ cryptoObject = createCredentialDecryptCryptoObject();
145
+ } else {
146
+ cryptoObject = createCryptoObject();
147
+ }
124
148
  if (cryptoObject == null) {
125
149
  finishActivity("error", 0, "Biometric crypto object unavailable");
126
150
  return;
@@ -234,6 +258,140 @@ public class AuthActivity extends AppCompatActivity {
234
258
  }
235
259
  }
236
260
 
261
+ private SecretKey getOrCreateCredentialKey(String server, int accessControl) throws GeneralSecurityException, IOException {
262
+ String alias = SECURE_KEY_PREFIX + server;
263
+ KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
264
+ ks.load(null);
265
+
266
+ if (ks.containsAlias(alias)) {
267
+ return (SecretKey) ks.getKey(alias, null);
268
+ }
269
+
270
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
271
+ KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
272
+ alias,
273
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
274
+ )
275
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
276
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
277
+ .setUserAuthenticationRequired(true);
278
+
279
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
280
+ builder.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG);
281
+ } else {
282
+ builder.setUserAuthenticationValidityDurationSeconds(1);
283
+ }
284
+
285
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
286
+ builder.setInvalidatedByBiometricEnrollment(accessControl == 1);
287
+ }
288
+
289
+ keyGenerator.init(builder.build());
290
+ return keyGenerator.generateKey();
291
+ }
292
+
293
+ private BiometricPrompt.CryptoObject createCredentialEncryptCryptoObject() {
294
+ try {
295
+ String server = getIntent().getStringExtra("server");
296
+ int accessControl = getIntent().getIntExtra("accessControl", 2);
297
+ SecretKey key = getOrCreateCredentialKey(server, accessControl);
298
+ Cipher cipher = Cipher.getInstance(AUTH_TRANSFORMATION);
299
+ try {
300
+ cipher.init(Cipher.ENCRYPT_MODE, key);
301
+ } catch (KeyPermanentlyInvalidatedException e) {
302
+ KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
303
+ ks.load(null);
304
+ ks.deleteEntry(SECURE_KEY_PREFIX + server);
305
+ key = getOrCreateCredentialKey(server, accessControl);
306
+ cipher.init(Cipher.ENCRYPT_MODE, key);
307
+ }
308
+ return new BiometricPrompt.CryptoObject(cipher);
309
+ } catch (GeneralSecurityException | IOException e) {
310
+ return null;
311
+ }
312
+ }
313
+
314
+ private BiometricPrompt.CryptoObject createCredentialDecryptCryptoObject() {
315
+ try {
316
+ String server = getIntent().getStringExtra("server");
317
+ SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
318
+ String encryptedData = prefs.getString("secure_" + server, null);
319
+ if (encryptedData == null) return null;
320
+
321
+ byte[] combined = Base64.decode(encryptedData, Base64.DEFAULT);
322
+ byte[] iv = new byte[CREDENTIAL_GCM_IV_LENGTH];
323
+ System.arraycopy(combined, 0, iv, 0, CREDENTIAL_GCM_IV_LENGTH);
324
+
325
+ SecretKey key = getOrCreateCredentialKey(server, 0);
326
+ Cipher cipher = Cipher.getInstance(AUTH_TRANSFORMATION);
327
+ cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
328
+ return new BiometricPrompt.CryptoObject(cipher);
329
+ } catch (GeneralSecurityException | IOException e) {
330
+ return null;
331
+ }
332
+ }
333
+
334
+ private void handleSetSecureCredentials(BiometricPrompt.AuthenticationResult result) {
335
+ try {
336
+ Cipher cipher = result.getCryptoObject().getCipher();
337
+ String username = getIntent().getStringExtra("username");
338
+ String password = getIntent().getStringExtra("password");
339
+ String server = getIntent().getStringExtra("server");
340
+
341
+ JSONObject json = new JSONObject();
342
+ json.put("u", username);
343
+ json.put("p", password);
344
+
345
+ byte[] encrypted = cipher.doFinal(json.toString().getBytes(StandardCharsets.UTF_8));
346
+ byte[] iv = cipher.getIV();
347
+
348
+ byte[] combined = new byte[iv.length + encrypted.length];
349
+ System.arraycopy(iv, 0, combined, 0, iv.length);
350
+ System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
351
+
352
+ String encoded = Base64.encodeToString(combined, Base64.DEFAULT);
353
+
354
+ SharedPreferences.Editor editor = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE).edit();
355
+ editor.putString("secure_" + server, encoded);
356
+ editor.apply();
357
+
358
+ finishActivity();
359
+ } catch (Exception e) {
360
+ finishActivity("error", 0, "Failed to encrypt credentials: " + e.getMessage());
361
+ }
362
+ }
363
+
364
+ private void handleGetSecureCredentials(BiometricPrompt.AuthenticationResult result) {
365
+ try {
366
+ Cipher cipher = result.getCryptoObject().getCipher();
367
+ String server = getIntent().getStringExtra("server");
368
+
369
+ SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
370
+ String encryptedData = prefs.getString("secure_" + server, null);
371
+ if (encryptedData == null) {
372
+ finishActivity("error", 21, "No protected credentials found");
373
+ return;
374
+ }
375
+
376
+ byte[] combined = Base64.decode(encryptedData, Base64.DEFAULT);
377
+ byte[] ciphertext = new byte[combined.length - CREDENTIAL_GCM_IV_LENGTH];
378
+ System.arraycopy(combined, CREDENTIAL_GCM_IV_LENGTH, ciphertext, 0, ciphertext.length);
379
+
380
+ byte[] decrypted = cipher.doFinal(ciphertext);
381
+ String jsonStr = new String(decrypted, StandardCharsets.UTF_8);
382
+ JSONObject json = new JSONObject(jsonStr);
383
+
384
+ Intent intent = new Intent();
385
+ intent.putExtra("result", "success");
386
+ intent.putExtra("username", json.getString("u"));
387
+ intent.putExtra("password", json.getString("p"));
388
+ setResult(RESULT_OK, intent);
389
+ finish();
390
+ } catch (Exception e) {
391
+ finishActivity("error", 0, "Failed to decrypt credentials: " + e.getMessage());
392
+ }
393
+ }
394
+
237
395
  /**
238
396
  * Convert Auth Error Codes to plugin expected Biometric Auth Errors (in README.md)
239
397
  * This way both iOS and Android return the same error codes for the same authentication failure reasons.
@@ -248,8 +248,24 @@ public class NativeBiometric extends Plugin {
248
248
  String username = call.getString("username", null);
249
249
  String password = call.getString("password", null);
250
250
  String KEY_ALIAS = call.getString("server", null);
251
+ Integer accessControl = call.getInt("accessControl", 0);
251
252
 
252
- if (username != null && password != null && KEY_ALIAS != null) {
253
+ if (username == null || password == null || KEY_ALIAS == null) {
254
+ call.reject("Missing properties");
255
+ return;
256
+ }
257
+
258
+ if (accessControl != null && accessControl > 0) {
259
+ Intent intent = new Intent(getContext(), AuthActivity.class);
260
+ intent.putExtra("mode", "setSecureCredentials");
261
+ intent.putExtra("server", KEY_ALIAS);
262
+ intent.putExtra("username", username);
263
+ intent.putExtra("password", password);
264
+ intent.putExtra("accessControl", accessControl);
265
+ intent.putExtra("title", "Protect Credentials");
266
+ intent.putExtra("negativeButtonText", "Cancel");
267
+ startActivityForResult(call, intent, "setSecureCredentialsResult");
268
+ } else {
253
269
  try {
254
270
  SharedPreferences.Editor editor = getContext()
255
271
  .getSharedPreferences(NATIVE_BIOMETRIC_SHARED_PREFERENCES, Context.MODE_PRIVATE)
@@ -260,13 +276,40 @@ public class NativeBiometric extends Plugin {
260
276
  call.resolve();
261
277
  } catch (GeneralSecurityException | IOException e) {
262
278
  call.reject("Failed to save credentials", e);
263
- System.out.println("Error saving credentials: " + e.getMessage());
264
279
  }
265
- } else {
266
- call.reject("Missing properties");
267
280
  }
268
281
  }
269
282
 
283
+ @PluginMethod
284
+ public void getSecureCredentials(final PluginCall call) {
285
+ String server = call.getString("server", null);
286
+ if (server == null) {
287
+ call.reject("No server name was provided");
288
+ return;
289
+ }
290
+
291
+ SharedPreferences sharedPreferences = getContext().getSharedPreferences(NATIVE_BIOMETRIC_SHARED_PREFERENCES, Context.MODE_PRIVATE);
292
+ String encryptedData = sharedPreferences.getString("secure_" + server, null);
293
+ if (encryptedData == null) {
294
+ call.reject("No protected credentials found", "21");
295
+ return;
296
+ }
297
+
298
+ Intent intent = new Intent(getContext(), AuthActivity.class);
299
+ intent.putExtra("mode", "getSecureCredentials");
300
+ intent.putExtra("server", server);
301
+ intent.putExtra("title", call.getString("title", "Authenticate"));
302
+
303
+ String subtitle = call.getString("subtitle");
304
+ if (subtitle != null) intent.putExtra("subtitle", subtitle);
305
+ String description = call.getString("description");
306
+ if (description != null) intent.putExtra("description", description);
307
+ String negativeText = call.getString("negativeButtonText");
308
+ if (negativeText != null) intent.putExtra("negativeButtonText", negativeText);
309
+
310
+ startActivityForResult(call, intent, "getSecureCredentialsResult");
311
+ }
312
+
270
313
  @PluginMethod
271
314
  public void getCredentials(final PluginCall call) {
272
315
  String KEY_ALIAS = call.getString("server", null);
@@ -318,6 +361,41 @@ public class NativeBiometric extends Plugin {
318
361
  }
319
362
  }
320
363
 
364
+ @ActivityCallback
365
+ private void setSecureCredentialsResult(PluginCall call, ActivityResult result) {
366
+ if (result.getResultCode() == Activity.RESULT_OK) {
367
+ Intent data = result.getData();
368
+ if (data != null && "success".equals(data.getStringExtra("result"))) {
369
+ call.resolve();
370
+ } else {
371
+ String errorCode = data != null ? data.getStringExtra("errorCode") : "0";
372
+ String errorDetails = data != null ? data.getStringExtra("errorDetails") : "Failed to store credentials";
373
+ call.reject(errorDetails, errorCode);
374
+ }
375
+ } else {
376
+ call.reject("Failed to store credentials");
377
+ }
378
+ }
379
+
380
+ @ActivityCallback
381
+ private void getSecureCredentialsResult(PluginCall call, ActivityResult result) {
382
+ if (result.getResultCode() == Activity.RESULT_OK) {
383
+ Intent data = result.getData();
384
+ if (data != null && "success".equals(data.getStringExtra("result"))) {
385
+ JSObject jsObject = new JSObject();
386
+ jsObject.put("username", data.getStringExtra("username"));
387
+ jsObject.put("password", data.getStringExtra("password"));
388
+ call.resolve(jsObject);
389
+ } else {
390
+ String errorCode = data != null ? data.getStringExtra("errorCode") : "0";
391
+ String errorDetails = data != null ? data.getStringExtra("errorDetails") : "Authentication failed";
392
+ call.reject(errorDetails, errorCode);
393
+ }
394
+ } else {
395
+ call.reject("Authentication failed");
396
+ }
397
+ }
398
+
321
399
  @PluginMethod
322
400
  public void deleteCredentials(final PluginCall call) {
323
401
  String KEY_ALIAS = call.getString("server", null);
@@ -330,6 +408,13 @@ public class NativeBiometric extends Plugin {
330
408
  .edit();
331
409
  editor.clear();
332
410
  editor.apply();
411
+
412
+ try {
413
+ getKeyStore().deleteEntry("NativeBiometricSecure_" + KEY_ALIAS);
414
+ } catch (KeyStoreException e) {
415
+ // Ignore — may not exist
416
+ }
417
+
333
418
  call.resolve();
334
419
  } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
335
420
  call.reject("Failed to delete", e);
@@ -351,8 +436,11 @@ public class NativeBiometric extends Plugin {
351
436
  String username = sharedPreferences.getString(KEY_ALIAS + "-username", null);
352
437
  String password = sharedPreferences.getString(KEY_ALIAS + "-password", null);
353
438
 
439
+ boolean hasUnprotected = username != null && password != null;
440
+ boolean hasProtected = sharedPreferences.getString("secure_" + KEY_ALIAS, null) != null;
441
+
354
442
  JSObject ret = new JSObject();
355
- ret.put("isSaved", username != null && password != null);
443
+ ret.put("isSaved", hasUnprotected || hasProtected);
356
444
  call.resolve(ret);
357
445
  } else {
358
446
  call.reject("No server name was provided");
package/dist/docs.json CHANGED
@@ -222,6 +222,41 @@
222
222
  ],
223
223
  "slug": "deletecredentials"
224
224
  },
225
+ {
226
+ "name": "getSecureCredentials",
227
+ "signature": "(options: GetSecureCredentialsOptions) => Promise<Credentials>",
228
+ "parameters": [
229
+ {
230
+ "name": "options",
231
+ "docs": "",
232
+ "type": "GetSecureCredentialsOptions"
233
+ }
234
+ ],
235
+ "returns": "Promise<Credentials>",
236
+ "tags": [
237
+ {
238
+ "name": "param",
239
+ "text": "options"
240
+ },
241
+ {
242
+ "name": "returns"
243
+ },
244
+ {
245
+ "name": "memberof",
246
+ "text": "NativeBiometricPlugin"
247
+ },
248
+ {
249
+ "name": "since",
250
+ "text": "8.4.0"
251
+ }
252
+ ],
253
+ "docs": "Gets the stored credentials for a given server, requiring biometric authentication.\nCredentials must have been stored with accessControl set to BIOMETRY_CURRENT_SET or BIOMETRY_ANY.\n\nOn iOS, the system automatically shows the biometric prompt when accessing the protected Keychain item.\nOn Android, BiometricPrompt is shown with a CryptoObject bound to the credential decryption key.",
254
+ "complexTypes": [
255
+ "Credentials",
256
+ "GetSecureCredentialsOptions"
257
+ ],
258
+ "slug": "getsecurecredentials"
259
+ },
225
260
  {
226
261
  "name": "isCredentialsSaved",
227
262
  "signature": "(options: IsCredentialsSavedOptions) => Promise<IsCredentialsSavedResult>",
@@ -524,6 +559,24 @@
524
559
  "docs": "",
525
560
  "complexTypes": [],
526
561
  "type": "string"
562
+ },
563
+ {
564
+ "name": "accessControl",
565
+ "tags": [
566
+ {
567
+ "text": "AccessControl.NONE",
568
+ "name": "default"
569
+ },
570
+ {
571
+ "text": "8.4.0",
572
+ "name": "since"
573
+ }
574
+ ],
575
+ "docs": "Access control level for the stored credentials.\nWhen set to BIOMETRY_CURRENT_SET or BIOMETRY_ANY, the credentials are\nhardware-protected and require biometric authentication to access.\n\nOn iOS, this adds SecAccessControl to the Keychain item.\nOn Android, this creates a biometric-protected Keystore key and requires\nBiometricPrompt authentication for both storing and retrieving credentials.",
576
+ "complexTypes": [
577
+ "AccessControl"
578
+ ],
579
+ "type": "AccessControl"
527
580
  }
528
581
  ]
529
582
  },
@@ -543,6 +596,57 @@
543
596
  }
544
597
  ]
545
598
  },
599
+ {
600
+ "name": "GetSecureCredentialsOptions",
601
+ "slug": "getsecurecredentialsoptions",
602
+ "docs": "",
603
+ "tags": [],
604
+ "methods": [],
605
+ "properties": [
606
+ {
607
+ "name": "server",
608
+ "tags": [],
609
+ "docs": "",
610
+ "complexTypes": [],
611
+ "type": "string"
612
+ },
613
+ {
614
+ "name": "reason",
615
+ "tags": [],
616
+ "docs": "Reason for requesting biometric authentication.\nDisplayed in the biometric prompt on both iOS and Android.",
617
+ "complexTypes": [],
618
+ "type": "string | undefined"
619
+ },
620
+ {
621
+ "name": "title",
622
+ "tags": [],
623
+ "docs": "Title for the biometric prompt.\nOnly for Android.",
624
+ "complexTypes": [],
625
+ "type": "string | undefined"
626
+ },
627
+ {
628
+ "name": "subtitle",
629
+ "tags": [],
630
+ "docs": "Subtitle for the biometric prompt.\nOnly for Android.",
631
+ "complexTypes": [],
632
+ "type": "string | undefined"
633
+ },
634
+ {
635
+ "name": "description",
636
+ "tags": [],
637
+ "docs": "Description for the biometric prompt.\nOnly for Android.",
638
+ "complexTypes": [],
639
+ "type": "string | undefined"
640
+ },
641
+ {
642
+ "name": "negativeButtonText",
643
+ "tags": [],
644
+ "docs": "Text for the negative/cancel button.\nOnly for Android.",
645
+ "complexTypes": [],
646
+ "type": "string | undefined"
647
+ }
648
+ ]
649
+ },
546
650
  {
547
651
  "name": "IsCredentialsSavedResult",
548
652
  "slug": "iscredentialssavedresult",
@@ -738,6 +842,30 @@
738
842
  "docs": "User chose to use fallback authentication method\nPlatform: Android, iOS"
739
843
  }
740
844
  ]
845
+ },
846
+ {
847
+ "name": "AccessControl",
848
+ "slug": "accesscontrol",
849
+ "members": [
850
+ {
851
+ "name": "NONE",
852
+ "value": "0",
853
+ "tags": [],
854
+ "docs": "No biometric protection. Credentials are accessible without authentication.\nThis is the default behavior for backward compatibility."
855
+ },
856
+ {
857
+ "name": "BIOMETRY_CURRENT_SET",
858
+ "value": "1",
859
+ "tags": [],
860
+ "docs": "Biometric authentication required for credential access.\nCredentials are invalidated if biometrics change (e.g., new fingerprint enrolled).\nMore secure but credentials are lost if user modifies their biometric enrollment."
861
+ },
862
+ {
863
+ "name": "BIOMETRY_ANY",
864
+ "value": "2",
865
+ "tags": [],
866
+ "docs": "Biometric authentication required for credential access.\nCredentials survive new biometric enrollment (e.g., adding a new fingerprint).\nMore lenient — recommended for most apps."
867
+ }
868
+ ]
741
869
  }
742
870
  ],
743
871
  "typeAliases": [