@capgo/capacitor-native-biometric 5.0.0 → 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.
- package/LICENSE +2 -2
- package/{readme.md → README.md} +51 -24
- package/android/gradlew +8 -4
- package/android/src/main/java/ee/forgr/biometric/AuthActivity.java +71 -33
- package/android/src/main/java/ee/forgr/biometric/NativeBiometric.java +118 -74
- package/dist/docs.json +27 -13
- package/dist/esm/definitions.d.ts +31 -3
- package/dist/esm/definitions.js +27 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +3 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +24 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +26 -3
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/Plugin.swift +60 -58
- package/ios/Podfile.lock +4 -4
- package/ios/Pods/Local Podspecs/Capacitor.podspec.json +2 -2
- package/ios/Pods/Local Podspecs/CapacitorCordova.podspec.json +2 -2
- package/ios/Pods/Manifest.lock +4 -4
- package/ios/Pods/Pods.xcodeproj/project.pbxproj +572 -572
- package/ios/Pods/Target Support Files/Capacitor/Capacitor-Info.plist +1 -1
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-Info.plist +1 -1
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh +0 -0
- package/package.json +17 -22
- package/android/.gradle/8.0.2/checksums/checksums.lock +0 -0
- package/android/.gradle/8.0.2/dependencies-accessors/dependencies-accessors.lock +0 -0
- package/android/.gradle/8.0.2/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.0.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.0.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.0.2/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.0.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.0.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.0.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/8.0.2/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/android.iml +0 -40
- package/android/local.properties +0 -8
- package/ios/Plugin.xcodeproj/project.xcworkspace/xcuserdata/pilito.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/Plugin.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/ios/Plugin.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/ios/Plugin.xcodeproj/xcuserdata/pilito.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/ios/Plugin.xcworkspace/xcuserdata/jmartinez.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/Plugin.xcworkspace/xcuserdata/josemartinez.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/Plugin.xcworkspace/xcuserdata/pilito.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/Capacitor.xcscheme +0 -60
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/xcschememanagement.plist +0 -39
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/Capacitor.xcscheme +0 -60
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/xcschememanagement.plist +0 -39
- 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)
|
|
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.
|
package/{readme.md → README.md}
RENAMED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Use biometrics confirm device owner presence or authenticate users. A couple of methods are provided to handle user credentials. These are securely stored using Keychain (iOS) and Keystore (Android).
|
|
4
4
|
|
|
5
|
-
## Installation (Only supports Capacitor
|
|
5
|
+
## Installation (Only supports Capacitor 5)
|
|
6
6
|
|
|
7
|
-
- `npm i capacitor-native-biometric`
|
|
7
|
+
- `npm i @capgo/capacitor-native-biometric`
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
import { NativeBiometric } from "capacitor-native-biometric";
|
|
12
|
+
import { NativeBiometric, BiometryType } from "@capgo/capacitor-native-biometric";
|
|
13
13
|
|
|
14
|
-
async
|
|
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
|
|
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
|
-
| **`
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
.
|
|
46
|
-
getIntent().
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.
|
|
51
|
-
getIntent().
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
getIntent().
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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"
|
|
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(
|
|
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
|
-
|
|
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,
|
|
122
|
+
void finishActivity(String result, Integer errorCode, String errorDetails) {
|
|
118
123
|
Intent intent = new Intent();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
intent.putExtra("errorDetails", result);
|
|
124
|
+
intent.putExtra("result", result);
|
|
125
|
+
if (errorCode != null) {
|
|
122
126
|
intent.putExtra("errorCode", String.valueOf(errorCode));
|
|
123
|
-
}
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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"))
|
|
138
|
-
"subtitle",
|
|
139
|
-
|
|
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("
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
);
|
|
159
|
+
if (call.hasOption("description")) {
|
|
160
|
+
intent.putExtra("description", call.getString("description"));
|
|
161
|
+
}
|
|
151
162
|
|
|
152
|
-
if (call.hasOption("
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
if (call.hasOption("negativeButtonText")) {
|
|
164
|
+
intent.putExtra(
|
|
165
|
+
"negativeButtonText",
|
|
166
|
+
call.getString("negativeButtonText")
|
|
167
|
+
);
|
|
168
|
+
}
|
|
156
169
|
|
|
157
|
-
|
|
170
|
+
if (call.hasOption("maxAttempts")) {
|
|
171
|
+
intent.putExtra("maxAttempts", call.getInt("maxAttempts"));
|
|
172
|
+
}
|
|
158
173
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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(
|
|
185
|
-
|
|
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(
|
|
210
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
call.reject(
|
|
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
|
-
|
|
248
|
-
|
|
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 (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|