@capgo/capacitor-native-biometric 7.1.2 → 7.1.7
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.
|
@@ -57,28 +57,21 @@ public class AuthActivity extends AppCompatActivity {
|
|
|
57
57
|
boolean useFallback = getIntent().getBooleanExtra("useFallback", false);
|
|
58
58
|
int[] allowedTypes = getIntent().getIntArrayExtra("allowedBiometryTypes");
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
getIntent().hasExtra("negativeButtonText")
|
|
76
|
-
? Objects.requireNonNull(
|
|
77
|
-
getIntent().getStringExtra("negativeButtonText")
|
|
78
|
-
)
|
|
79
|
-
: "Cancel"
|
|
80
|
-
);
|
|
81
|
-
}
|
|
60
|
+
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
|
|
61
|
+
if (useFallback) {
|
|
62
|
+
authenticators |= BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
|
63
|
+
}
|
|
64
|
+
if (allowedTypes != null) {
|
|
65
|
+
// Filter authenticators based on allowed types
|
|
66
|
+
authenticators = getAllowedAuthenticators(allowedTypes);
|
|
67
|
+
}
|
|
68
|
+
builder.setAllowedAuthenticators(authenticators);
|
|
69
|
+
|
|
70
|
+
if (!useFallback) {
|
|
71
|
+
String negativeText = getIntent().getStringExtra("negativeButtonText");
|
|
72
|
+
builder.setNegativeButtonText(
|
|
73
|
+
negativeText != null ? negativeText : "Cancel"
|
|
74
|
+
);
|
|
82
75
|
}
|
|
83
76
|
|
|
84
77
|
BiometricPrompt.PromptInfo promptInfo = builder.build();
|
|
@@ -93,9 +86,16 @@ public class AuthActivity extends AppCompatActivity {
|
|
|
93
86
|
@NonNull CharSequence errString
|
|
94
87
|
) {
|
|
95
88
|
super.onAuthenticationError(errorCode, errString);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
// Handle lockout cases explicitly
|
|
90
|
+
if (
|
|
91
|
+
errorCode == BiometricPrompt.ERROR_LOCKOUT ||
|
|
92
|
+
errorCode == BiometricPrompt.ERROR_LOCKOUT_PERMANENT
|
|
93
|
+
) {
|
|
94
|
+
int pluginErrorCode = convertToPluginErrorCode(errorCode);
|
|
95
|
+
finishActivity("error", pluginErrorCode, errString.toString());
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
int pluginErrorCode = convertToPluginErrorCode(errorCode);
|
|
99
99
|
finishActivity("error", pluginErrorCode, errString.toString());
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -111,11 +111,10 @@ public class AuthActivity extends AppCompatActivity {
|
|
|
111
111
|
public void onAuthenticationFailed() {
|
|
112
112
|
super.onAuthenticationFailed();
|
|
113
113
|
counter++;
|
|
114
|
-
if (counter
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
114
|
+
if (counter >= maxAttempts) {
|
|
115
|
+
// Use error code 4 for too many attempts to match iOS behavior
|
|
116
|
+
finishActivity("error", 4, "Too many failed attempts");
|
|
117
|
+
}
|
|
119
118
|
}
|
|
120
119
|
}
|
|
121
120
|
);
|
|
@@ -153,11 +152,11 @@ public class AuthActivity extends AppCompatActivity {
|
|
|
153
152
|
case BiometricPrompt.ERROR_HW_NOT_PRESENT:
|
|
154
153
|
return 1;
|
|
155
154
|
case BiometricPrompt.ERROR_LOCKOUT_PERMANENT:
|
|
156
|
-
return 2;
|
|
155
|
+
return 2; // Permanent lockout
|
|
157
156
|
case BiometricPrompt.ERROR_NO_BIOMETRICS:
|
|
158
157
|
return 3;
|
|
159
158
|
case BiometricPrompt.ERROR_LOCKOUT:
|
|
160
|
-
return 4;
|
|
159
|
+
return 4; // Temporary lockout (too many attempts)
|
|
161
160
|
// Authentication Failure (10) Handled by `onAuthenticationFailed`.
|
|
162
161
|
// App Cancel (11), Invalid Context (12), and Not Interactive (13) are not valid error codes for Android.
|
|
163
162
|
case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL:
|
|
@@ -168,6 +167,8 @@ public class AuthActivity extends AppCompatActivity {
|
|
|
168
167
|
case BiometricPrompt.ERROR_USER_CANCELED:
|
|
169
168
|
case BiometricPrompt.ERROR_NEGATIVE_BUTTON:
|
|
170
169
|
return 16;
|
|
170
|
+
case BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC:
|
|
171
|
+
return 0; // Success case, should not be handled here
|
|
171
172
|
default:
|
|
172
173
|
return 0;
|
|
173
174
|
}
|
|
@@ -8,7 +8,6 @@ import android.content.Intent;
|
|
|
8
8
|
import android.content.SharedPreferences;
|
|
9
9
|
import android.content.pm.PackageManager;
|
|
10
10
|
import android.os.Build;
|
|
11
|
-
import android.security.KeyPairGeneratorSpec;
|
|
12
11
|
import android.security.keystore.KeyGenParameterSpec;
|
|
13
12
|
import android.security.keystore.KeyProperties;
|
|
14
13
|
import android.security.keystore.StrongBoxUnavailableException;
|
|
@@ -74,42 +73,55 @@ public class NativeBiometric extends Plugin {
|
|
|
74
73
|
|
|
75
74
|
private int getAvailableFeature() {
|
|
76
75
|
// default to none
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// if has fingerprint
|
|
80
|
-
if (
|
|
81
|
-
getContext()
|
|
82
|
-
.getPackageManager()
|
|
83
|
-
.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
|
|
84
|
-
) {
|
|
85
|
-
type = FINGERPRINT;
|
|
86
|
-
}
|
|
76
|
+
BiometricManager biometricManager = BiometricManager.from(getContext());
|
|
87
77
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
.getPackageManager()
|
|
92
|
-
.hasSystemFeature(PackageManager.FEATURE_FACE)
|
|
93
|
-
) {
|
|
94
|
-
// if also has fingerprint
|
|
95
|
-
if (type != NONE) return MULTIPLE;
|
|
78
|
+
// Check for biometric capabilities
|
|
79
|
+
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
|
|
80
|
+
int canAuthenticate = biometricManager.canAuthenticate(authenticators);
|
|
96
81
|
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
|
|
83
|
+
// Check specific features
|
|
84
|
+
PackageManager pm = getContext().getPackageManager();
|
|
85
|
+
boolean hasFinger = pm.hasSystemFeature(
|
|
86
|
+
PackageManager.FEATURE_FINGERPRINT
|
|
87
|
+
);
|
|
88
|
+
boolean hasIris = false;
|
|
89
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
90
|
+
hasIris = pm.hasSystemFeature(PackageManager.FEATURE_IRIS);
|
|
91
|
+
}
|
|
99
92
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
93
|
+
// For face, we rely on BiometricManager since it's more reliable
|
|
94
|
+
boolean hasFace = false;
|
|
95
|
+
try {
|
|
96
|
+
// Try to create a face authentication prompt - if it succeeds, face auth is available
|
|
97
|
+
androidx.biometric.BiometricPrompt.PromptInfo promptInfo =
|
|
98
|
+
new androidx.biometric.BiometricPrompt.PromptInfo.Builder()
|
|
99
|
+
.setTitle("Test")
|
|
100
|
+
.setNegativeButtonText("Cancel")
|
|
101
|
+
.setAllowedAuthenticators(
|
|
102
|
+
BiometricManager.Authenticators.BIOMETRIC_STRONG
|
|
103
|
+
)
|
|
104
|
+
.build();
|
|
105
|
+
hasFace = true;
|
|
106
|
+
} catch (Exception e) {
|
|
107
|
+
System.out.println(
|
|
108
|
+
"Error creating face authentication prompt: " + e.getMessage()
|
|
109
|
+
);
|
|
110
|
+
}
|
|
108
111
|
|
|
109
|
-
type
|
|
112
|
+
// Determine the type based on available features
|
|
113
|
+
if (hasFinger && (hasFace || hasIris)) {
|
|
114
|
+
return MULTIPLE;
|
|
115
|
+
} else if (hasFinger) {
|
|
116
|
+
return FINGERPRINT;
|
|
117
|
+
} else if (hasFace) {
|
|
118
|
+
return FACE_AUTHENTICATION;
|
|
119
|
+
} else if (hasIris) {
|
|
120
|
+
return IRIS_AUTHENTICATION;
|
|
121
|
+
}
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
return
|
|
124
|
+
return NONE;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
@PluginMethod
|
|
@@ -121,7 +133,13 @@ public class NativeBiometric extends Plugin {
|
|
|
121
133
|
);
|
|
122
134
|
|
|
123
135
|
BiometricManager biometricManager = BiometricManager.from(getContext());
|
|
124
|
-
int
|
|
136
|
+
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
|
|
137
|
+
if (useFallback) {
|
|
138
|
+
authenticators |= BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
|
139
|
+
}
|
|
140
|
+
int canAuthenticateResult = biometricManager.canAuthenticate(
|
|
141
|
+
authenticators
|
|
142
|
+
);
|
|
125
143
|
// Using deviceHasCredentials instead of canAuthenticate(DEVICE_CREDENTIAL)
|
|
126
144
|
// > "Developers that wish to check for the presence of a PIN, pattern, or password on these versions should instead use isDeviceSecure."
|
|
127
145
|
// @see https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
|
|
@@ -153,23 +171,24 @@ public class NativeBiometric extends Plugin {
|
|
|
153
171
|
|
|
154
172
|
intent.putExtra("title", call.getString("title", "Authenticate"));
|
|
155
173
|
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
String subtitle = call.getString("subtitle");
|
|
175
|
+
if (subtitle != null) {
|
|
176
|
+
intent.putExtra("subtitle", subtitle);
|
|
158
177
|
}
|
|
159
178
|
|
|
160
|
-
|
|
161
|
-
|
|
179
|
+
String description = call.getString("description");
|
|
180
|
+
if (description != null) {
|
|
181
|
+
intent.putExtra("description", description);
|
|
162
182
|
}
|
|
163
183
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
call.getString("negativeButtonText")
|
|
168
|
-
);
|
|
184
|
+
String negativeButtonText = call.getString("negativeButtonText");
|
|
185
|
+
if (negativeButtonText != null) {
|
|
186
|
+
intent.putExtra("negativeButtonText", negativeButtonText);
|
|
169
187
|
}
|
|
170
188
|
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
Integer maxAttempts = call.getInt("maxAttempts");
|
|
190
|
+
if (maxAttempts != null) {
|
|
191
|
+
intent.putExtra("maxAttempts", maxAttempts);
|
|
173
192
|
}
|
|
174
193
|
|
|
175
194
|
// Pass allowed biometry types
|
|
@@ -220,7 +239,7 @@ public class NativeBiometric extends Plugin {
|
|
|
220
239
|
call.resolve();
|
|
221
240
|
} catch (GeneralSecurityException | IOException e) {
|
|
222
241
|
call.reject("Failed to save credentials", e);
|
|
223
|
-
e.
|
|
242
|
+
System.out.println("Error saving credentials: " + e.getMessage());
|
|
224
243
|
}
|
|
225
244
|
} else {
|
|
226
245
|
call.reject("Missing properties");
|
|
@@ -443,8 +462,12 @@ public class NativeBiometric extends Plugin {
|
|
|
443
462
|
ANDROID_KEY_STORE
|
|
444
463
|
);
|
|
445
464
|
keyPairGenerator.initialize(
|
|
446
|
-
new
|
|
447
|
-
|
|
465
|
+
new KeyGenParameterSpec.Builder(
|
|
466
|
+
KEY_ALIAS,
|
|
467
|
+
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
|
|
468
|
+
)
|
|
469
|
+
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
|
|
470
|
+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
|
|
448
471
|
.build()
|
|
449
472
|
);
|
|
450
473
|
keyPairGenerator.generateKeyPair();
|
|
@@ -471,8 +494,7 @@ public class NativeBiometric extends Plugin {
|
|
|
471
494
|
cipherOutputStream.write(secret);
|
|
472
495
|
cipherOutputStream.close();
|
|
473
496
|
|
|
474
|
-
|
|
475
|
-
return vals;
|
|
497
|
+
return outputStream.toByteArray();
|
|
476
498
|
}
|
|
477
499
|
|
|
478
500
|
private byte[] rsaDecrypt(byte[] encrypted, String KEY_ALIAS)
|
package/package.json
CHANGED