@capgo/capacitor-native-biometric 5.0.1 → 5.1.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.
Files changed (60) hide show
  1. package/LICENSE +2 -2
  2. package/{readme.md → README.md} +49 -22
  3. package/android/gradlew +8 -4
  4. package/android/src/main/java/ee/forgr/biometric/AuthActivity.java +71 -33
  5. package/android/src/main/java/ee/forgr/biometric/NativeBiometric.java +118 -74
  6. package/dist/docs.json +27 -13
  7. package/dist/esm/definitions.d.ts +31 -3
  8. package/dist/esm/definitions.js +27 -0
  9. package/dist/esm/definitions.js.map +1 -1
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/esm/web.d.ts +3 -3
  12. package/dist/esm/web.js.map +1 -1
  13. package/dist/plugin.cjs.js +24 -1
  14. package/dist/plugin.cjs.js.map +1 -1
  15. package/dist/plugin.js +26 -3
  16. package/dist/plugin.js.map +1 -1
  17. package/ios/Plugin/Plugin.swift +60 -58
  18. package/ios/Podfile.lock +4 -4
  19. package/ios/Pods/Local Podspecs/Capacitor.podspec.json +2 -2
  20. package/ios/Pods/Local Podspecs/CapacitorCordova.podspec.json +2 -2
  21. package/ios/Pods/Manifest.lock +4 -4
  22. package/ios/Pods/Pods.xcodeproj/project.pbxproj +572 -572
  23. package/ios/Pods/Target Support Files/Capacitor/Capacitor-Info.plist +1 -1
  24. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-Info.plist +1 -1
  25. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh +0 -0
  26. package/package.json +17 -22
  27. package/android/.gradle/8.0.2/checksums/checksums.lock +0 -0
  28. package/android/.gradle/8.0.2/dependencies-accessors/dependencies-accessors.lock +0 -0
  29. package/android/.gradle/8.0.2/dependencies-accessors/gc.properties +0 -0
  30. package/android/.gradle/8.0.2/executionHistory/executionHistory.bin +0 -0
  31. package/android/.gradle/8.0.2/executionHistory/executionHistory.lock +0 -0
  32. package/android/.gradle/8.0.2/fileChanges/last-build.bin +0 -0
  33. package/android/.gradle/8.0.2/fileHashes/fileHashes.bin +0 -0
  34. package/android/.gradle/8.0.2/fileHashes/fileHashes.lock +0 -0
  35. package/android/.gradle/8.0.2/fileHashes/resourceHashesCache.bin +0 -0
  36. package/android/.gradle/8.0.2/gc.properties +0 -0
  37. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  38. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  39. package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  40. package/android/.gradle/vcs-1/gc.properties +0 -0
  41. package/android/android.iml +0 -40
  42. package/android/local.properties +0 -8
  43. package/ios/Plugin.xcodeproj/project.xcworkspace/xcuserdata/pilito.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  44. package/ios/Plugin.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  45. package/ios/Plugin.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  46. package/ios/Plugin.xcodeproj/xcuserdata/pilito.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  47. package/ios/Plugin.xcworkspace/xcuserdata/jmartinez.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  48. package/ios/Plugin.xcworkspace/xcuserdata/josemartinez.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  49. package/ios/Plugin.xcworkspace/xcuserdata/pilito.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  50. package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/Capacitor.xcscheme +0 -60
  51. package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +0 -58
  52. package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +0 -58
  53. package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +0 -58
  54. package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/xcschememanagement.plist +0 -39
  55. package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/Capacitor.xcscheme +0 -60
  56. package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +0 -58
  57. package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +0 -58
  58. package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +0 -58
  59. package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/xcschememanagement.plist +0 -39
  60. package/ios/Pods/Pods.xcodeproj/xcuserdata/pilito.xcuserdatad/xcschemes/xcschememanagement.plist +0 -29
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) Ariel Hernandez Musa.
3
+ Copyright (c) Martin Donadieu.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
@@ -9,9 +9,9 @@ Use biometrics confirm device owner presence or authenticate users. A couple of
9
9
  ## Usage
10
10
 
11
11
  ```ts
12
- import { NativeBiometric } from "@capgo/capacitor-native-biometric";
12
+ import { NativeBiometric, BiometryType } from "@capgo/capacitor-native-biometric";
13
13
 
14
- async performBiometricVerificatin(){
14
+ async performBiometricVerification(){
15
15
  const result = await NativeBiometric.isAvailable();
16
16
 
17
17
  if(!result.isAvailable) return;
@@ -46,6 +46,27 @@ NativeBiometric.deleteCredentials({
46
46
  server: "www.example.com",
47
47
  }).then();
48
48
  ```
49
+
50
+ ### Biometric Auth Errors
51
+
52
+ This is a plugin specific list of error codes that can be thrown on verifyIdentity failure, or set as a part of isAvailable. It consolidates Android and iOS specific Authentication Error codes into one combined error list.
53
+
54
+ | Code | Description | Platform |
55
+ | ---- | --------------------------- | ---------------------------- |
56
+ | 0 | Unknown Error | Android, iOS |
57
+ | 1 | Biometrics Unavailable | Android, iOS |
58
+ | 2 | User Lockout | Android, iOS |
59
+ | 3 | Biometrics Not Enrolled | Android, iOS |
60
+ | 4 | User Temporary Lockout | Android (Lockout for 30sec) |
61
+ | 10 | Authentication Failed | Android, iOS |
62
+ | 11 | App Cancel | iOS |
63
+ | 12 | Invalid Context | iOS |
64
+ | 13 | Not Interactive | iOS |
65
+ | 14 | Passcode Not Set | Android, iOS |
66
+ | 15 | System Cancel | Android, iOS |
67
+ | 16 | User Cancel | Android, iOS |
68
+ | 17 | User Fallback | Android, iOS |
69
+
49
70
  <docgen-index>
50
71
 
51
72
  * [`isAvailable(...)`](#isavailable)
@@ -64,7 +85,7 @@ NativeBiometric.deleteCredentials({
64
85
  ### isAvailable(...)
65
86
 
66
87
  ```typescript
67
- isAvailable(options?: IsAvailableOptions) => any
88
+ isAvailable(options?: IsAvailableOptions | undefined) => any
68
89
  ```
69
90
 
70
91
  Checks if biometric authentication hardware is available.
@@ -83,7 +104,7 @@ Checks if biometric authentication hardware is available.
83
104
  ### verifyIdentity(...)
84
105
 
85
106
  ```typescript
86
- verifyIdentity(options?: BiometricOptions) => any
107
+ verifyIdentity(options?: BiometricOptions | undefined) => any
87
108
  ```
88
109
 
89
110
  Prompts the user to authenticate with biometrics.
@@ -177,15 +198,16 @@ Deletes the stored credentials for a given server.
177
198
 
178
199
  #### BiometricOptions
179
200
 
180
- | Prop | Type | Description | Default |
181
- | ------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------- |
182
- | **`reason`** | <code>string</code> | | |
183
- | **`title`** | <code>string</code> | | |
184
- | **`subtitle`** | <code>string</code> | | |
185
- | **`description`** | <code>string</code> | | |
186
- | **`negativeButtonText`** | <code>string</code> | | |
187
- | **`useFallback`** | <code>boolean</code> | | |
188
- | **`maxAttempts`** | <code>number</code> | Only for Android. Set a maximum number of attempts for biometric authentication. The maximum allowed by android is 5. | <code>1</code> |
201
+ | Prop | Type | Description | Default |
202
+ | ------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
203
+ | **`reason`** | <code>string</code> | | |
204
+ | **`title`** | <code>string</code> | | |
205
+ | **`subtitle`** | <code>string</code> | | |
206
+ | **`description`** | <code>string</code> | | |
207
+ | **`negativeButtonText`** | <code>string</code> | | |
208
+ | **`useFallback`** | <code>boolean</code> | Specifies if should fallback to passcode authentication if biometric authentication fails. | |
209
+ | **`fallbackTitle`** | <code>string</code> | Only for iOS. Set the text for the fallback button in the authentication dialog. If this property is not specified, the default text is set by the system. | |
210
+ | **`maxAttempts`** | <code>number</code> | Only for Android. Set a maximum number of attempts for biometric authentication. The maximum allowed by android is 5. | <code>1</code> |
189
211
 
190
212
 
191
213
  #### GetCredentialOptions
@@ -224,15 +246,15 @@ Deletes the stored credentials for a given server.
224
246
 
225
247
  #### BiometryType
226
248
 
227
- | Members |
228
- | ------------------------- |
229
- | **`NONE`** |
230
- | **`TOUCH_ID`** |
231
- | **`FACE_ID`** |
232
- | **`FINGERPRINT`** |
233
- | **`FACE_AUTHENTICATION`** |
234
- | **`IRIS_AUTHENTICATION`** |
235
- | **`MULTIPLE`** |
249
+ | Members | Value |
250
+ | ------------------------- | -------------- |
251
+ | **`NONE`** | <code>0</code> |
252
+ | **`TOUCH_ID`** | <code>1</code> |
253
+ | **`FACE_ID`** | <code>2</code> |
254
+ | **`FINGERPRINT`** | <code>3</code> |
255
+ | **`FACE_AUTHENTICATION`** | <code>4</code> |
256
+ | **`IRIS_AUTHENTICATION`** | <code>5</code> |
257
+ | **`MULTIPLE`** | <code>6</code> |
236
258
 
237
259
  </docgen-api>
238
260
  ## Face ID (iOS)
@@ -278,6 +300,11 @@ public class MainActivity extends BridgeActivity {
278
300
 
279
301
  [Jonthia](https://github.com/jonthia)
280
302
  [One Click Web Studio](https://github.com/oneclickwebstudio)
303
+ [Brian Weasner](https://github.com/brian-weasner)
304
+ [Mohamed Diarra](https://github.com/mohdiarra)
305
+ ### Want to Contribute?
306
+
307
+ Learn about contributing [HERE](./CONTRIBUTING.md)
281
308
 
282
309
  ## Notes
283
310
 
package/android/gradlew CHANGED
@@ -85,9 +85,6 @@ done
85
85
  APP_BASE_NAME=${0##*/}
86
86
  APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87
87
 
88
- # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89
- DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90
-
91
88
  # Use the maximum available, or set MAX_FD != -1 to use that value.
92
89
  MAX_FD=maximum
93
90
 
@@ -133,10 +130,13 @@ location of your Java installation."
133
130
  fi
134
131
  else
135
132
  JAVACMD=java
136
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
133
+ if ! command -v java >/dev/null 2>&1
134
+ then
135
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137
136
 
138
137
  Please set the JAVA_HOME variable in your environment to match the
139
138
  location of your Java installation."
139
+ fi
140
140
  fi
141
141
 
142
142
  # Increase the maximum file descriptors if we can.
@@ -197,6 +197,10 @@ if "$cygwin" || "$msys" ; then
197
197
  done
198
198
  fi
199
199
 
200
+
201
+ # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
202
+ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
203
+
200
204
  # Collect all arguments for the java command;
201
205
  # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
202
206
  # shell script including quotes and variable substitutions, so put them in
@@ -9,10 +9,11 @@ import android.view.View;
9
9
  import androidx.annotation.NonNull;
10
10
  import androidx.appcompat.app.AppCompatActivity;
11
11
  import androidx.appcompat.widget.Toolbar;
12
+ import androidx.biometric.BiometricConstants;
12
13
  import androidx.biometric.BiometricPrompt;
13
- import ee.forgr.biometric.capacitornativebiometric.R;
14
14
  import com.google.android.material.floatingactionbutton.FloatingActionButton;
15
15
  import com.google.android.material.snackbar.Snackbar;
16
+ import ee.forgr.biometric.capacitornativebiometric.R;
16
17
  import java.util.concurrent.Executor;
17
18
 
18
19
  public class AuthActivity extends AppCompatActivity {
@@ -40,29 +41,31 @@ public class AuthActivity extends AppCompatActivity {
40
41
  };
41
42
  }
42
43
 
43
- BiometricPrompt.PromptInfo.Builder builder =
44
- new BiometricPrompt.PromptInfo.Builder()
45
- .setTitle(
46
- getIntent().hasExtra("title")
47
- ? getIntent().getStringExtra("title")
48
- : "Authenticate"
49
- )
50
- .setSubtitle(
51
- getIntent().hasExtra("subtitle")
52
- ? getIntent().getStringExtra("subtitle")
53
- : null
54
- )
55
- .setDescription(
56
- getIntent().hasExtra("description")
57
- ? getIntent().getStringExtra("description")
58
- : null
59
- );
44
+ BiometricPrompt.PromptInfo.Builder builder = new BiometricPrompt.PromptInfo.Builder()
45
+ .setTitle(
46
+ getIntent().hasExtra("title")
47
+ ? getIntent().getStringExtra("title")
48
+ : "Authenticate"
49
+ )
50
+ .setSubtitle(
51
+ getIntent().hasExtra("subtitle")
52
+ ? getIntent().getStringExtra("subtitle")
53
+ : null
54
+ )
55
+ .setDescription(
56
+ getIntent().hasExtra("description")
57
+ ? getIntent().getStringExtra("description")
58
+ : null
59
+ );
60
60
 
61
61
  boolean useFallback = getIntent().getBooleanExtra("useFallback", false);
62
62
 
63
63
  if (useFallback) {
64
+ // TODO: Deprecated function, probably want to migrate to `setAllowedAuthenticators`
64
65
  builder.setDeviceCredentialAllowed(true);
65
66
  } else {
67
+ // Note that this option is incompatible with device credential authentication and must NOT be set if the latter is enabled via `setAllowedAuthenticators` or `setDeviceCredentialAllowed`.
68
+ // @see https://developer.android.com/reference/androidx/biometric/BiometricPrompt.PromptInfo.Builder#setNegativeButtonText(java.lang.CharSequence)
66
69
  builder.setNegativeButtonText(
67
70
  getIntent().hasExtra("negativeButtonText")
68
71
  ? getIntent().getStringExtra("negativeButtonText")
@@ -82,8 +85,10 @@ public class AuthActivity extends AppCompatActivity {
82
85
  @NonNull CharSequence errString
83
86
  ) {
84
87
  super.onAuthenticationError(errorCode, errString);
85
-
86
- finishActivity(errString.toString(), errorCode);
88
+ int pluginErrorCode = AuthActivity.convertToPluginErrorCode(
89
+ errorCode
90
+ );
91
+ finishActivity("error", pluginErrorCode, errString.toString());
87
92
  }
88
93
 
89
94
  @Override
@@ -91,14 +96,18 @@ public class AuthActivity extends AppCompatActivity {
91
96
  @NonNull BiometricPrompt.AuthenticationResult result
92
97
  ) {
93
98
  super.onAuthenticationSucceeded(result);
94
- finishActivity("success", 0);
99
+ finishActivity("success");
95
100
  }
96
101
 
97
102
  @Override
98
103
  public void onAuthenticationFailed() {
99
104
  super.onAuthenticationFailed();
100
105
  counter++;
101
- if (counter == maxAttempts) finishActivity("failed");
106
+ if (counter == maxAttempts) finishActivity(
107
+ "failed",
108
+ 10,
109
+ "Authentication failed."
110
+ );
102
111
  }
103
112
  }
104
113
  );
@@ -107,23 +116,52 @@ public class AuthActivity extends AppCompatActivity {
107
116
  }
108
117
 
109
118
  void finishActivity(String result) {
110
- Intent intent = new Intent();
111
- intent.putExtra("result", "failed");
112
- intent.putExtra("errorDetails", "Authentication failed.");
113
- setResult(RESULT_OK, intent);
114
- finish();
119
+ finishActivity(result, null, null);
115
120
  }
116
121
 
117
- void finishActivity(String result, int errorCode) {
122
+ void finishActivity(String result, Integer errorCode, String errorDetails) {
118
123
  Intent intent = new Intent();
119
- if (errorCode != 0) {
120
- intent.putExtra("result", "error");
121
- intent.putExtra("errorDetails", result);
124
+ intent.putExtra("result", result);
125
+ if (errorCode != null) {
122
126
  intent.putExtra("errorCode", String.valueOf(errorCode));
123
- } else {
124
- intent.putExtra("result", result);
127
+ }
128
+ if (errorDetails != null) {
129
+ intent.putExtra("errorDetails", errorDetails);
125
130
  }
126
131
  setResult(RESULT_OK, intent);
127
132
  finish();
128
133
  }
134
+
135
+ /**
136
+ * Convert Auth Error Codes to plugin expected Biometric Auth Errors (in README.md)
137
+ * This way both iOS and Android return the same error codes for the same authentication failure reasons.
138
+ * !!IMPORTANT!!: Whenever this is modified, check if similar function in iOS Plugin.swift needs to be modified as well
139
+ * @see https://developer.android.com/reference/androidx/biometric/BiometricPrompt#constants
140
+ * @return BiometricAuthError
141
+ */
142
+ public static int convertToPluginErrorCode(int errorCode) {
143
+ switch (errorCode) {
144
+ case BiometricConstants.ERROR_HW_UNAVAILABLE:
145
+ case BiometricConstants.ERROR_HW_NOT_PRESENT:
146
+ return 1;
147
+ case BiometricConstants.ERROR_LOCKOUT_PERMANENT:
148
+ return 2;
149
+ case BiometricConstants.ERROR_NO_BIOMETRICS:
150
+ return 3;
151
+ case BiometricConstants.ERROR_LOCKOUT:
152
+ return 4;
153
+ // Authentication Failure (10) Handled by `onAuthenticationFailed`.
154
+ // App Cancel (11), Invalid Context (12), and Not Interactive (13) are not valid error codes for Android.
155
+ case BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL:
156
+ return 14;
157
+ case BiometricConstants.ERROR_TIMEOUT:
158
+ case BiometricConstants.ERROR_CANCELED:
159
+ return 15;
160
+ case BiometricConstants.ERROR_USER_CANCELED:
161
+ case BiometricConstants.ERROR_NEGATIVE_BUTTON:
162
+ return 16;
163
+ default:
164
+ return 0;
165
+ }
166
+ }
129
167
  }
@@ -1,5 +1,6 @@
1
1
  package ee.forgr.biometric;
2
2
 
3
+ import android.annotation.SuppressLint;
3
4
  import android.app.Activity;
4
5
  import android.app.KeyguardManager;
5
6
  import android.content.Context;
@@ -10,8 +11,10 @@ import android.os.Build;
10
11
  import android.security.KeyPairGeneratorSpec;
11
12
  import android.security.keystore.KeyGenParameterSpec;
12
13
  import android.security.keystore.KeyProperties;
14
+ import android.security.keystore.StrongBoxUnavailableException;
13
15
  import android.util.Base64;
14
16
  import androidx.activity.result.ActivityResult;
17
+ import androidx.biometric.BiometricConstants;
15
18
  import androidx.biometric.BiometricManager;
16
19
  import com.getcapacitor.JSObject;
17
20
  import com.getcapacitor.Plugin;
@@ -46,7 +49,6 @@ import javax.crypto.spec.SecretKeySpec;
46
49
  @CapacitorPlugin(name = "NativeBiometric")
47
50
  public class NativeBiometric extends Plugin {
48
51
 
49
- private BiometricManager biometricManager;
50
52
  //protected final static int AUTH_CODE = 0102;
51
53
 
52
54
  private static final int NONE = 0;
@@ -111,17 +113,33 @@ public class NativeBiometric extends Plugin {
111
113
  public void isAvailable(PluginCall call) {
112
114
  JSObject ret = new JSObject();
113
115
 
114
- biometricManager = BiometricManager.from(getContext());
116
+ boolean useFallback = Boolean.TRUE.equals(
117
+ call.getBoolean("useFallback", false)
118
+ );
119
+
120
+ BiometricManager biometricManager = BiometricManager.from(getContext());
115
121
  int canAuthenticateResult = biometricManager.canAuthenticate();
122
+ // Using deviceHasCredentials instead of canAuthenticate(DEVICE_CREDENTIAL)
123
+ // > "Developers that wish to check for the presence of a PIN, pattern, or password on these versions should instead use isDeviceSecure."
124
+ // @see https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
125
+ boolean fallbackAvailable = useFallback && this.deviceHasCredentials();
126
+ if (useFallback && !fallbackAvailable) {
127
+ canAuthenticateResult = BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL;
128
+ }
129
+
130
+ boolean isAvailable =
131
+ (
132
+ canAuthenticateResult == BiometricManager.BIOMETRIC_SUCCESS ||
133
+ fallbackAvailable
134
+ );
135
+ ret.put("isAvailable", isAvailable);
116
136
 
117
- switch (canAuthenticateResult) {
118
- case BiometricManager.BIOMETRIC_SUCCESS:
119
- ret.put("isAvailable", true);
120
- break;
121
- default:
122
- ret.put("isAvailable", false);
123
- ret.put("errorCode", canAuthenticateResult);
124
- break;
137
+ if (!isAvailable) {
138
+ // BiometricManager Error Constants use the same values as BiometricPrompt's Constants. So we can reuse our
139
+ int pluginErrorCode = AuthActivity.convertToPluginErrorCode(
140
+ canAuthenticateResult
141
+ );
142
+ ret.put("errorCode", pluginErrorCode);
125
143
  }
126
144
 
127
145
  ret.put("biometryType", getAvailableFeature());
@@ -134,32 +152,30 @@ public class NativeBiometric extends Plugin {
134
152
 
135
153
  intent.putExtra("title", call.getString("title", "Authenticate"));
136
154
 
137
- if (call.hasOption("subtitle")) intent.putExtra(
138
- "subtitle",
139
- call.getString("subtitle")
140
- );
141
-
142
- if (call.hasOption("description")) intent.putExtra(
143
- "description",
144
- call.getString("description")
145
- );
155
+ if (call.hasOption("subtitle")) {
156
+ intent.putExtra("subtitle", call.getString("subtitle"));
157
+ }
146
158
 
147
- if (call.hasOption("negativeButtonText")) intent.putExtra(
148
- "negativeButtonText",
149
- call.getString("negativeButtonText")
150
- );
159
+ if (call.hasOption("description")) {
160
+ intent.putExtra("description", call.getString("description"));
161
+ }
151
162
 
152
- if (call.hasOption("maxAttempts")) intent.putExtra(
153
- "maxAttempts",
154
- call.getInt("maxAttempts")
155
- );
163
+ if (call.hasOption("negativeButtonText")) {
164
+ intent.putExtra(
165
+ "negativeButtonText",
166
+ call.getString("negativeButtonText")
167
+ );
168
+ }
156
169
 
157
- boolean useFallback = call.getBoolean("useFallback", false);
170
+ if (call.hasOption("maxAttempts")) {
171
+ intent.putExtra("maxAttempts", call.getInt("maxAttempts"));
172
+ }
158
173
 
159
- if (useFallback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
160
- KeyguardManager keyguardManager = (KeyguardManager) getActivity()
161
- .getSystemService(Context.KEYGUARD_SERVICE);
162
- useFallback = keyguardManager.isDeviceSecure();
174
+ boolean useFallback = Boolean.TRUE.equals(
175
+ call.getBoolean("useFallback", false)
176
+ );
177
+ if (useFallback) {
178
+ useFallback = this.deviceHasCredentials();
163
179
  }
164
180
 
165
181
  intent.putExtra("useFallback", useFallback);
@@ -181,14 +197,17 @@ public class NativeBiometric extends Plugin {
181
197
  Context.MODE_PRIVATE
182
198
  )
183
199
  .edit();
184
- editor.putString("username", encryptString(username, KEY_ALIAS));
185
- editor.putString("password", encryptString(password, KEY_ALIAS));
200
+ editor.putString(
201
+ KEY_ALIAS + "-username",
202
+ encryptString(username, KEY_ALIAS)
203
+ );
204
+ editor.putString(
205
+ KEY_ALIAS + "-password",
206
+ encryptString(password, KEY_ALIAS)
207
+ );
186
208
  editor.apply();
187
209
  call.resolve();
188
- } catch (GeneralSecurityException e) {
189
- call.reject("Failed to save credentials", e);
190
- e.printStackTrace();
191
- } catch (IOException e) {
210
+ } catch (GeneralSecurityException | IOException e) {
192
211
  call.reject("Failed to save credentials", e);
193
212
  e.printStackTrace();
194
213
  }
@@ -206,8 +225,14 @@ public class NativeBiometric extends Plugin {
206
225
  NATIVE_BIOMETRIC_SHARED_PREFERENCES,
207
226
  Context.MODE_PRIVATE
208
227
  );
209
- String username = sharedPreferences.getString("username", null);
210
- String password = sharedPreferences.getString("password", null);
228
+ String username = sharedPreferences.getString(
229
+ KEY_ALIAS + "-username",
230
+ null
231
+ );
232
+ String password = sharedPreferences.getString(
233
+ KEY_ALIAS + "-password",
234
+ null
235
+ );
211
236
  if (KEY_ALIAS != null) {
212
237
  if (username != null && password != null) {
213
238
  try {
@@ -215,10 +240,10 @@ public class NativeBiometric extends Plugin {
215
240
  jsObject.put("username", decryptString(username, KEY_ALIAS));
216
241
  jsObject.put("password", decryptString(password, KEY_ALIAS));
217
242
  call.resolve(jsObject);
218
- } catch (GeneralSecurityException e) {
219
- call.reject("Failed to get credentials", e);
220
- } catch (IOException e) {
221
- call.reject("Failed to get credentials", e);
243
+ } catch (GeneralSecurityException | IOException e) {
244
+ // Can get here if not authenticated.
245
+ String errorMessage = "Failed to get credentials";
246
+ call.reject(errorMessage);
222
247
  }
223
248
  } else {
224
249
  call.reject("No credentials found");
@@ -232,22 +257,21 @@ public class NativeBiometric extends Plugin {
232
257
  private void verifyResult(PluginCall call, ActivityResult result) {
233
258
  if (result.getResultCode() == Activity.RESULT_OK) {
234
259
  Intent data = result.getData();
235
- if (data.hasExtra("result")) {
260
+ if (data != null && data.hasExtra("result")) {
236
261
  switch (data.getStringExtra("result")) {
237
262
  case "success":
238
263
  call.resolve();
239
264
  break;
240
265
  case "failed":
266
+ case "error":
241
267
  call.reject(
242
268
  data.getStringExtra("errorDetails"),
243
269
  data.getStringExtra("errorCode")
244
270
  );
245
271
  break;
246
272
  default:
247
- call.reject(
248
- "Verification error: " + data.getStringExtra("result"),
249
- data.getStringExtra("errorCode")
250
- );
273
+ // Should not get to here unless AuthActivity starts returning different Activity Results.
274
+ call.reject("Something went wrong.");
251
275
  break;
252
276
  }
253
277
  }
@@ -272,13 +296,12 @@ public class NativeBiometric extends Plugin {
272
296
  editor.clear();
273
297
  editor.apply();
274
298
  call.resolve();
275
- } catch (KeyStoreException e) {
276
- call.reject("Failed to delete", e);
277
- } catch (CertificateException e) {
278
- call.reject("Failed to delete", e);
279
- } catch (NoSuchAlgorithmException e) {
280
- call.reject("Failed to delete", e);
281
- } catch (IOException e) {
299
+ } catch (
300
+ KeyStoreException
301
+ | CertificateException
302
+ | NoSuchAlgorithmException
303
+ | IOException e
304
+ ) {
282
305
  call.reject("Failed to delete", e);
283
306
  }
284
307
  } else {
@@ -324,23 +347,39 @@ public class NativeBiometric extends Plugin {
324
347
  return new String(decryptedData, "UTF-8");
325
348
  }
326
349
 
350
+ @SuppressLint("NewAPI") // API level is already checked
327
351
  private Key generateKey(String KEY_ALIAS)
328
352
  throws GeneralSecurityException, IOException {
353
+ Key key;
354
+ try {
355
+ key = generateKey(KEY_ALIAS, true);
356
+ } catch (StrongBoxUnavailableException e) {
357
+ key = generateKey(KEY_ALIAS, false);
358
+ }
359
+ return key;
360
+ }
361
+
362
+ private Key generateKey(String KEY_ALIAS, boolean isStrongBoxBacked)
363
+ throws GeneralSecurityException, IOException, StrongBoxUnavailableException {
329
364
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
330
365
  KeyGenerator generator = KeyGenerator.getInstance(
331
366
  KeyProperties.KEY_ALGORITHM_AES,
332
367
  ANDROID_KEY_STORE
333
368
  );
334
- generator.init(
335
- new KeyGenParameterSpec.Builder(
336
- KEY_ALIAS,
337
- KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
338
- )
339
- .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
340
- .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
341
- .setRandomizedEncryptionRequired(false)
342
- .build()
343
- );
369
+ KeyGenParameterSpec.Builder paramBuilder = new KeyGenParameterSpec.Builder(
370
+ KEY_ALIAS,
371
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
372
+ )
373
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
374
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
375
+ .setRandomizedEncryptionRequired(false);
376
+
377
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
378
+ paramBuilder.setUnlockedDeviceRequired(true);
379
+ paramBuilder.setIsStrongBoxBacked(isStrongBoxBacked);
380
+ }
381
+
382
+ generator.init(paramBuilder.build());
344
383
  return generator.generateKey();
345
384
  } else {
346
385
  return getAESKey(KEY_ALIAS);
@@ -349,8 +388,8 @@ public class NativeBiometric extends Plugin {
349
388
 
350
389
  private Key getKey(String KEY_ALIAS)
351
390
  throws GeneralSecurityException, IOException {
352
- KeyStore.SecretKeyEntry secretKeyEntry =
353
- (KeyStore.SecretKeyEntry) getKeyStore().getEntry(KEY_ALIAS, null);
391
+ KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) getKeyStore()
392
+ .getEntry(KEY_ALIAS, null);
354
393
  if (secretKeyEntry != null) {
355
394
  return secretKeyEntry.getSecretKey();
356
395
  }
@@ -359,13 +398,11 @@ public class NativeBiometric extends Plugin {
359
398
 
360
399
  private KeyStore getKeyStore()
361
400
  throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
362
- if (keyStore != null) {
363
- return keyStore;
364
- } else {
401
+ if (keyStore == null) {
365
402
  keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
366
403
  keyStore.load(null);
367
- return keyStore;
368
404
  }
405
+ return keyStore;
369
406
  }
370
407
 
371
408
  private Key getAESKey(String KEY_ALIAS)
@@ -392,8 +429,8 @@ public class NativeBiometric extends Plugin {
392
429
 
393
430
  private KeyStore.PrivateKeyEntry getPrivateKeyEntry(String KEY_ALIAS)
394
431
  throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, CertificateException, KeyStoreException, IOException, UnrecoverableEntryException {
395
- KeyStore.PrivateKeyEntry privateKeyEntry =
396
- (KeyStore.PrivateKeyEntry) getKeyStore().getEntry(KEY_ALIAS, null);
432
+ KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) getKeyStore()
433
+ .getEntry(KEY_ALIAS, null);
397
434
 
398
435
  if (privateKeyEntry == null) {
399
436
  KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
@@ -454,4 +491,11 @@ public class NativeBiometric extends Plugin {
454
491
  }
455
492
  return bytes;
456
493
  }
494
+
495
+ private boolean deviceHasCredentials() {
496
+ KeyguardManager keyguardManager = (KeyguardManager) getActivity()
497
+ .getSystemService(Context.KEYGUARD_SERVICE);
498
+ // Can only use fallback if the device has a pin/pattern/password lockscreen.
499
+ return keyguardManager.isDeviceSecure();
500
+ }
457
501
  }