@capgo/capacitor-native-biometric 8.3.7 → 8.4.1
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 +51 -5
- package/android/build.gradle +1 -1
- package/android/src/main/java/ee/forgr/biometric/AuthActivity.java +164 -6
- package/android/src/main/java/ee/forgr/biometric/NativeBiometric.java +93 -5
- package/dist/docs.json +128 -0
- package/dist/esm/definitions.d.ts +73 -0
- package/dist/esm/definitions.js +20 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +8 -1
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +28 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +28 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeBiometricPlugin/NativeBiometricPlugin.swift +133 -11
- package/package.json +1 -1
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<<a href="#credentials">Credentials</a>></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
|
|
506
|
-
|
|
|
507
|
-
| **`username`**
|
|
508
|
-
| **`password`**
|
|
509
|
-
| **`server`**
|
|
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
|
|
package/android/build.gradle
CHANGED
|
@@ -30,7 +30,7 @@ android {
|
|
|
30
30
|
buildTypes {
|
|
31
31
|
release {
|
|
32
32
|
minifyEnabled false
|
|
33
|
-
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
33
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
lintOptions {
|
|
@@ -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
|
-
|
|
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 (
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
|
|
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",
|
|
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": [
|