@capgo/capacitor-native-biometric 7.1.13 → 7.1.15
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/Package.swift +28 -0
- package/README.md +4 -0
- package/android/src/main/java/ee/forgr/biometric/AuthActivity.java +141 -166
- package/android/src/main/java/ee/forgr/biometric/NativeBiometric.java +351 -443
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +4 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +2 -2
- package/dist/esm/web.js +10 -10
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +10 -10
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +10 -10
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin/Plugin.swift → Sources/CapgoNativeBiometricPlugin/CapgoNativeBiometricPlugin.swift} +3 -3
- package/ios/Tests/CapgoNativeBiometricPluginTests/CapgoNativeBiometricPluginTests.swift +13 -0
- package/package.json +20 -19
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/android/gradle.properties +0 -20
- package/android/gradlew +0 -252
- package/android/gradlew.bat +0 -94
- package/android/proguard-rules.pro +0 -21
- package/android/settings.gradle +0 -2
- package/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +0 -27
- package/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +0 -18
- package/ios/Plugin/Info.plist +0 -24
- package/ios/Plugin.xcodeproj/project.pbxproj +0 -546
- package/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
- package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/ios/PluginTests/Info.plist +0 -22
- package/ios/PluginTests/PluginTests.swift +0 -25
- package/ios/Podfile +0 -16
|
@@ -51,487 +51,395 @@ import org.json.JSONException;
|
|
|
51
51
|
@CapacitorPlugin(name = "NativeBiometric")
|
|
52
52
|
public class NativeBiometric extends Plugin {
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
117
|
-
} else if (hasFace) {
|
|
118
|
-
return FACE_AUTHENTICATION;
|
|
119
|
-
} else if (hasIris) {
|
|
120
|
-
return IRIS_AUTHENTICATION;
|
|
121
|
-
}
|
|
54
|
+
//protected final static int AUTH_CODE = 0102;
|
|
55
|
+
|
|
56
|
+
private static final int NONE = 0;
|
|
57
|
+
private static final int FINGERPRINT = 3;
|
|
58
|
+
private static final int FACE_AUTHENTICATION = 4;
|
|
59
|
+
private static final int IRIS_AUTHENTICATION = 5;
|
|
60
|
+
private static final int MULTIPLE = 6;
|
|
61
|
+
|
|
62
|
+
private KeyStore keyStore;
|
|
63
|
+
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
|
|
64
|
+
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
|
65
|
+
private static final String RSA_MODE = "RSA/ECB/PKCS1Padding";
|
|
66
|
+
private static final String AES_MODE = "AES/ECB/PKCS7Padding";
|
|
67
|
+
private static final byte[] FIXED_IV = new byte[12];
|
|
68
|
+
private static final String ENCRYPTED_KEY = "NativeBiometricKey";
|
|
69
|
+
private static final String NATIVE_BIOMETRIC_SHARED_PREFERENCES = "NativeBiometricSharedPreferences";
|
|
70
|
+
|
|
71
|
+
private SharedPreferences encryptedSharedPreferences;
|
|
72
|
+
|
|
73
|
+
private int getAvailableFeature() {
|
|
74
|
+
// default to none
|
|
75
|
+
BiometricManager biometricManager = BiometricManager.from(getContext());
|
|
76
|
+
|
|
77
|
+
// Check for biometric capabilities
|
|
78
|
+
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
|
|
79
|
+
int canAuthenticate = biometricManager.canAuthenticate(authenticators);
|
|
80
|
+
|
|
81
|
+
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
|
|
82
|
+
// Check specific features
|
|
83
|
+
PackageManager pm = getContext().getPackageManager();
|
|
84
|
+
boolean hasFinger = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
|
|
85
|
+
boolean hasIris = false;
|
|
86
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
87
|
+
hasIris = pm.hasSystemFeature(PackageManager.FEATURE_IRIS);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// For face, we rely on BiometricManager since it's more reliable
|
|
91
|
+
boolean hasFace = false;
|
|
92
|
+
try {
|
|
93
|
+
// Try to create a face authentication prompt - if it succeeds, face auth is available
|
|
94
|
+
androidx.biometric.BiometricPrompt.PromptInfo promptInfo = new androidx.biometric.BiometricPrompt.PromptInfo.Builder()
|
|
95
|
+
.setTitle("Test")
|
|
96
|
+
.setNegativeButtonText("Cancel")
|
|
97
|
+
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
|
98
|
+
.build();
|
|
99
|
+
hasFace = true;
|
|
100
|
+
} catch (Exception e) {
|
|
101
|
+
System.out.println("Error creating face authentication prompt: " + e.getMessage());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Determine the type based on available features
|
|
105
|
+
if (hasFinger && (hasFace || hasIris)) {
|
|
106
|
+
return MULTIPLE;
|
|
107
|
+
} else if (hasFinger) {
|
|
108
|
+
return FINGERPRINT;
|
|
109
|
+
} else if (hasFace) {
|
|
110
|
+
return FACE_AUTHENTICATION;
|
|
111
|
+
} else if (hasIris) {
|
|
112
|
+
return IRIS_AUTHENTICATION;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return NONE;
|
|
122
117
|
}
|
|
123
118
|
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
@PluginMethod
|
|
120
|
+
public void isAvailable(PluginCall call) {
|
|
121
|
+
JSObject ret = new JSObject();
|
|
126
122
|
|
|
127
|
-
|
|
128
|
-
public void isAvailable(PluginCall call) {
|
|
129
|
-
JSObject ret = new JSObject();
|
|
123
|
+
boolean useFallback = Boolean.TRUE.equals(call.getBoolean("useFallback", false));
|
|
130
124
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
BiometricManager biometricManager = BiometricManager.from(getContext());
|
|
126
|
+
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
|
|
127
|
+
if (useFallback) {
|
|
128
|
+
authenticators |= BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
|
129
|
+
}
|
|
130
|
+
int canAuthenticateResult = biometricManager.canAuthenticate(authenticators);
|
|
131
|
+
// Using deviceHasCredentials instead of canAuthenticate(DEVICE_CREDENTIAL)
|
|
132
|
+
// > "Developers that wish to check for the presence of a PIN, pattern, or password on these versions should instead use isDeviceSecure."
|
|
133
|
+
// @see https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
|
|
134
|
+
boolean fallbackAvailable = useFallback && this.deviceHasCredentials();
|
|
135
|
+
if (useFallback && !fallbackAvailable) {
|
|
136
|
+
canAuthenticateResult = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
|
|
137
|
+
}
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (useFallback) {
|
|
138
|
-
authenticators |= BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
|
139
|
-
}
|
|
140
|
-
int canAuthenticateResult = biometricManager.canAuthenticate(
|
|
141
|
-
authenticators
|
|
142
|
-
);
|
|
143
|
-
// Using deviceHasCredentials instead of canAuthenticate(DEVICE_CREDENTIAL)
|
|
144
|
-
// > "Developers that wish to check for the presence of a PIN, pattern, or password on these versions should instead use isDeviceSecure."
|
|
145
|
-
// @see https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
|
|
146
|
-
boolean fallbackAvailable = useFallback && this.deviceHasCredentials();
|
|
147
|
-
if (useFallback && !fallbackAvailable) {
|
|
148
|
-
canAuthenticateResult = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
|
|
149
|
-
}
|
|
139
|
+
boolean isAvailable = (canAuthenticateResult == BiometricManager.BIOMETRIC_SUCCESS || fallbackAvailable);
|
|
140
|
+
ret.put("isAvailable", isAvailable);
|
|
150
141
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
canAuthenticateResult
|
|
160
|
-
);
|
|
161
|
-
ret.put("errorCode", pluginErrorCode);
|
|
142
|
+
if (!isAvailable) {
|
|
143
|
+
// BiometricManager Error Constants use the same values as BiometricPrompt's Constants. So we can reuse our
|
|
144
|
+
int pluginErrorCode = AuthActivity.convertToPluginErrorCode(canAuthenticateResult);
|
|
145
|
+
ret.put("errorCode", pluginErrorCode);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
ret.put("biometryType", getAvailableFeature());
|
|
149
|
+
call.resolve(ret);
|
|
162
150
|
}
|
|
163
151
|
|
|
164
|
-
|
|
165
|
-
call
|
|
166
|
-
|
|
152
|
+
@PluginMethod
|
|
153
|
+
public void verifyIdentity(final PluginCall call) throws JSONException {
|
|
154
|
+
Intent intent = new Intent(getContext(), AuthActivity.class);
|
|
155
|
+
|
|
156
|
+
intent.putExtra("title", call.getString("title", "Authenticate"));
|
|
157
|
+
|
|
158
|
+
String subtitle = call.getString("subtitle");
|
|
159
|
+
if (subtitle != null) {
|
|
160
|
+
intent.putExtra("subtitle", subtitle);
|
|
161
|
+
}
|
|
167
162
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
String description = call.getString("description");
|
|
164
|
+
if (description != null) {
|
|
165
|
+
intent.putExtra("description", description);
|
|
166
|
+
}
|
|
171
167
|
|
|
172
|
-
|
|
168
|
+
String negativeButtonText = call.getString("negativeButtonText");
|
|
169
|
+
if (negativeButtonText != null) {
|
|
170
|
+
intent.putExtra("negativeButtonText", negativeButtonText);
|
|
171
|
+
}
|
|
173
172
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
Integer maxAttempts = call.getInt("maxAttempts");
|
|
174
|
+
if (maxAttempts != null) {
|
|
175
|
+
intent.putExtra("maxAttempts", maxAttempts);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Pass allowed biometry types
|
|
179
|
+
JSArray allowedTypes = call.getArray("allowedBiometryTypes");
|
|
180
|
+
if (allowedTypes != null) {
|
|
181
|
+
int[] types = new int[allowedTypes.length()];
|
|
182
|
+
for (int i = 0; i < allowedTypes.length(); i++) {
|
|
183
|
+
types[i] = (int) allowedTypes.toList().get(i);
|
|
184
|
+
}
|
|
185
|
+
intent.putExtra("allowedBiometryTypes", types);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
boolean useFallback = Boolean.TRUE.equals(call.getBoolean("useFallback", false));
|
|
189
|
+
if (useFallback) {
|
|
190
|
+
useFallback = this.deviceHasCredentials();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
intent.putExtra("useFallback", useFallback);
|
|
194
|
+
|
|
195
|
+
startActivityForResult(call, intent, "verifyResult");
|
|
177
196
|
}
|
|
178
197
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
198
|
+
@PluginMethod
|
|
199
|
+
public void setCredentials(final PluginCall call) {
|
|
200
|
+
String username = call.getString("username", null);
|
|
201
|
+
String password = call.getString("password", null);
|
|
202
|
+
String KEY_ALIAS = call.getString("server", null);
|
|
203
|
+
|
|
204
|
+
if (username != null && password != null && KEY_ALIAS != null) {
|
|
205
|
+
try {
|
|
206
|
+
SharedPreferences.Editor editor = getContext()
|
|
207
|
+
.getSharedPreferences(NATIVE_BIOMETRIC_SHARED_PREFERENCES, Context.MODE_PRIVATE)
|
|
208
|
+
.edit();
|
|
209
|
+
editor.putString(KEY_ALIAS + "-username", encryptString(username, KEY_ALIAS));
|
|
210
|
+
editor.putString(KEY_ALIAS + "-password", encryptString(password, KEY_ALIAS));
|
|
211
|
+
editor.apply();
|
|
212
|
+
call.resolve();
|
|
213
|
+
} catch (GeneralSecurityException | IOException e) {
|
|
214
|
+
call.reject("Failed to save credentials", e);
|
|
215
|
+
System.out.println("Error saving credentials: " + e.getMessage());
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
call.reject("Missing properties");
|
|
219
|
+
}
|
|
182
220
|
}
|
|
183
221
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
222
|
+
@PluginMethod
|
|
223
|
+
public void getCredentials(final PluginCall call) {
|
|
224
|
+
String KEY_ALIAS = call.getString("server", null);
|
|
225
|
+
|
|
226
|
+
SharedPreferences sharedPreferences = getContext().getSharedPreferences(NATIVE_BIOMETRIC_SHARED_PREFERENCES, Context.MODE_PRIVATE);
|
|
227
|
+
String username = sharedPreferences.getString(KEY_ALIAS + "-username", null);
|
|
228
|
+
String password = sharedPreferences.getString(KEY_ALIAS + "-password", null);
|
|
229
|
+
if (KEY_ALIAS != null) {
|
|
230
|
+
if (username != null && password != null) {
|
|
231
|
+
try {
|
|
232
|
+
JSObject jsObject = new JSObject();
|
|
233
|
+
jsObject.put("username", decryptString(username, KEY_ALIAS));
|
|
234
|
+
jsObject.put("password", decryptString(password, KEY_ALIAS));
|
|
235
|
+
call.resolve(jsObject);
|
|
236
|
+
} catch (GeneralSecurityException | IOException e) {
|
|
237
|
+
// Can get here if not authenticated.
|
|
238
|
+
String errorMessage = "Failed to get credentials";
|
|
239
|
+
call.reject(errorMessage);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
call.reject("No credentials found");
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
call.reject("No server name was provided");
|
|
246
|
+
}
|
|
187
247
|
}
|
|
188
248
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
249
|
+
@ActivityCallback
|
|
250
|
+
private void verifyResult(PluginCall call, ActivityResult result) {
|
|
251
|
+
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
252
|
+
Intent data = result.getData();
|
|
253
|
+
if (data != null && data.hasExtra("result")) {
|
|
254
|
+
switch (Objects.requireNonNull(data.getStringExtra("result"))) {
|
|
255
|
+
case "success":
|
|
256
|
+
call.resolve();
|
|
257
|
+
break;
|
|
258
|
+
case "failed":
|
|
259
|
+
case "error":
|
|
260
|
+
call.reject(data.getStringExtra("errorDetails"), data.getStringExtra("errorCode"));
|
|
261
|
+
break;
|
|
262
|
+
default:
|
|
263
|
+
// Should not get to here unless AuthActivity starts returning different Activity Results.
|
|
264
|
+
call.reject("Something went wrong.");
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
call.reject("Something went wrong.");
|
|
270
|
+
}
|
|
192
271
|
}
|
|
193
272
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
273
|
+
@PluginMethod
|
|
274
|
+
public void deleteCredentials(final PluginCall call) {
|
|
275
|
+
String KEY_ALIAS = call.getString("server", null);
|
|
276
|
+
|
|
277
|
+
if (KEY_ALIAS != null) {
|
|
278
|
+
try {
|
|
279
|
+
getKeyStore().deleteEntry(KEY_ALIAS);
|
|
280
|
+
SharedPreferences.Editor editor = getContext()
|
|
281
|
+
.getSharedPreferences(NATIVE_BIOMETRIC_SHARED_PREFERENCES, Context.MODE_PRIVATE)
|
|
282
|
+
.edit();
|
|
283
|
+
editor.clear();
|
|
284
|
+
editor.apply();
|
|
285
|
+
call.resolve();
|
|
286
|
+
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
|
|
287
|
+
call.reject("Failed to delete", e);
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
call.reject("No server name was provided");
|
|
291
|
+
}
|
|
202
292
|
}
|
|
203
293
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
294
|
+
private String encryptString(String stringToEncrypt, String KEY_ALIAS) throws GeneralSecurityException, IOException {
|
|
295
|
+
Cipher cipher;
|
|
296
|
+
cipher = Cipher.getInstance(TRANSFORMATION);
|
|
297
|
+
cipher.init(Cipher.ENCRYPT_MODE, getKey(KEY_ALIAS), new GCMParameterSpec(128, FIXED_IV));
|
|
298
|
+
byte[] encodedBytes = cipher.doFinal(stringToEncrypt.getBytes(StandardCharsets.UTF_8));
|
|
299
|
+
return Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
|
209
300
|
}
|
|
210
301
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
String password = call.getString("password", null);
|
|
220
|
-
String KEY_ALIAS = call.getString("server", null);
|
|
221
|
-
|
|
222
|
-
if (username != null && password != null && KEY_ALIAS != null) {
|
|
223
|
-
try {
|
|
224
|
-
SharedPreferences.Editor editor = getContext()
|
|
225
|
-
.getSharedPreferences(
|
|
226
|
-
NATIVE_BIOMETRIC_SHARED_PREFERENCES,
|
|
227
|
-
Context.MODE_PRIVATE
|
|
228
|
-
)
|
|
229
|
-
.edit();
|
|
230
|
-
editor.putString(
|
|
231
|
-
KEY_ALIAS + "-username",
|
|
232
|
-
encryptString(username, KEY_ALIAS)
|
|
233
|
-
);
|
|
234
|
-
editor.putString(
|
|
235
|
-
KEY_ALIAS + "-password",
|
|
236
|
-
encryptString(password, KEY_ALIAS)
|
|
237
|
-
);
|
|
238
|
-
editor.apply();
|
|
239
|
-
call.resolve();
|
|
240
|
-
} catch (GeneralSecurityException | IOException e) {
|
|
241
|
-
call.reject("Failed to save credentials", e);
|
|
242
|
-
System.out.println("Error saving credentials: " + e.getMessage());
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
call.reject("Missing properties");
|
|
302
|
+
private String decryptString(String stringToDecrypt, String KEY_ALIAS) throws GeneralSecurityException, IOException {
|
|
303
|
+
byte[] encryptedData = Base64.decode(stringToDecrypt, Base64.DEFAULT);
|
|
304
|
+
|
|
305
|
+
Cipher cipher;
|
|
306
|
+
cipher = Cipher.getInstance(TRANSFORMATION);
|
|
307
|
+
cipher.init(Cipher.DECRYPT_MODE, getKey(KEY_ALIAS), new GCMParameterSpec(128, FIXED_IV));
|
|
308
|
+
byte[] decryptedData = cipher.doFinal(encryptedData);
|
|
309
|
+
return new String(decryptedData, StandardCharsets.UTF_8);
|
|
246
310
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
String KEY_ALIAS = call.getString("server", null);
|
|
252
|
-
|
|
253
|
-
SharedPreferences sharedPreferences = getContext()
|
|
254
|
-
.getSharedPreferences(
|
|
255
|
-
NATIVE_BIOMETRIC_SHARED_PREFERENCES,
|
|
256
|
-
Context.MODE_PRIVATE
|
|
257
|
-
);
|
|
258
|
-
String username = sharedPreferences.getString(
|
|
259
|
-
KEY_ALIAS + "-username",
|
|
260
|
-
null
|
|
261
|
-
);
|
|
262
|
-
String password = sharedPreferences.getString(
|
|
263
|
-
KEY_ALIAS + "-password",
|
|
264
|
-
null
|
|
265
|
-
);
|
|
266
|
-
if (KEY_ALIAS != null) {
|
|
267
|
-
if (username != null && password != null) {
|
|
311
|
+
|
|
312
|
+
@SuppressLint("NewAPI") // API level is already checked
|
|
313
|
+
private Key generateKey(String KEY_ALIAS) throws GeneralSecurityException, IOException {
|
|
314
|
+
Key key;
|
|
268
315
|
try {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
call.resolve(jsObject);
|
|
273
|
-
} catch (GeneralSecurityException | IOException e) {
|
|
274
|
-
// Can get here if not authenticated.
|
|
275
|
-
String errorMessage = "Failed to get credentials";
|
|
276
|
-
call.reject(errorMessage);
|
|
316
|
+
key = generateKey(KEY_ALIAS, true);
|
|
317
|
+
} catch (StrongBoxUnavailableException e) {
|
|
318
|
+
key = generateKey(KEY_ALIAS, false);
|
|
277
319
|
}
|
|
278
|
-
|
|
279
|
-
call.reject("No credentials found");
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
call.reject("No server name was provided");
|
|
320
|
+
return key;
|
|
283
321
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
default:
|
|
303
|
-
// Should not get to here unless AuthActivity starts returning different Activity Results.
|
|
304
|
-
call.reject("Something went wrong.");
|
|
305
|
-
break;
|
|
322
|
+
|
|
323
|
+
private Key generateKey(String KEY_ALIAS, boolean isStrongBoxBacked)
|
|
324
|
+
throws GeneralSecurityException, IOException, StrongBoxUnavailableException {
|
|
325
|
+
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
|
|
326
|
+
KeyGenParameterSpec.Builder paramBuilder = new KeyGenParameterSpec.Builder(
|
|
327
|
+
KEY_ALIAS,
|
|
328
|
+
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
|
|
329
|
+
)
|
|
330
|
+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
331
|
+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
332
|
+
.setRandomizedEncryptionRequired(false);
|
|
333
|
+
|
|
334
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
335
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || Build.VERSION.SDK_INT > 34) {
|
|
336
|
+
// Avoiding setUnlockedDeviceRequired(true) due to known issues on Android 12-14
|
|
337
|
+
paramBuilder.setUnlockedDeviceRequired(true);
|
|
338
|
+
}
|
|
339
|
+
paramBuilder.setIsStrongBoxBacked(isStrongBoxBacked);
|
|
306
340
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
341
|
+
|
|
342
|
+
generator.init(paramBuilder.build());
|
|
343
|
+
return generator.generateKey();
|
|
310
344
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
getKeyStore().deleteEntry(KEY_ALIAS);
|
|
320
|
-
SharedPreferences.Editor editor = getContext()
|
|
321
|
-
.getSharedPreferences(
|
|
322
|
-
NATIVE_BIOMETRIC_SHARED_PREFERENCES,
|
|
323
|
-
Context.MODE_PRIVATE
|
|
324
|
-
)
|
|
325
|
-
.edit();
|
|
326
|
-
editor.clear();
|
|
327
|
-
editor.apply();
|
|
328
|
-
call.resolve();
|
|
329
|
-
} catch (
|
|
330
|
-
KeyStoreException
|
|
331
|
-
| CertificateException
|
|
332
|
-
| NoSuchAlgorithmException
|
|
333
|
-
| IOException e
|
|
334
|
-
) {
|
|
335
|
-
call.reject("Failed to delete", e);
|
|
336
|
-
}
|
|
337
|
-
} else {
|
|
338
|
-
call.reject("No server name was provided");
|
|
345
|
+
|
|
346
|
+
private Key getKey(String KEY_ALIAS) throws GeneralSecurityException, IOException {
|
|
347
|
+
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) getKeyStore().getEntry(KEY_ALIAS, null);
|
|
348
|
+
if (secretKeyEntry != null) {
|
|
349
|
+
return secretKeyEntry.getSecretKey();
|
|
350
|
+
}
|
|
351
|
+
return generateKey(KEY_ALIAS);
|
|
339
352
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
Cipher.ENCRYPT_MODE,
|
|
348
|
-
getKey(KEY_ALIAS),
|
|
349
|
-
new GCMParameterSpec(128, FIXED_IV)
|
|
350
|
-
);
|
|
351
|
-
byte[] encodedBytes = cipher.doFinal(
|
|
352
|
-
stringToEncrypt.getBytes(StandardCharsets.UTF_8)
|
|
353
|
-
);
|
|
354
|
-
return Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private String decryptString(String stringToDecrypt, String KEY_ALIAS)
|
|
358
|
-
throws GeneralSecurityException, IOException {
|
|
359
|
-
byte[] encryptedData = Base64.decode(stringToDecrypt, Base64.DEFAULT);
|
|
360
|
-
|
|
361
|
-
Cipher cipher;
|
|
362
|
-
cipher = Cipher.getInstance(TRANSFORMATION);
|
|
363
|
-
cipher.init(
|
|
364
|
-
Cipher.DECRYPT_MODE,
|
|
365
|
-
getKey(KEY_ALIAS),
|
|
366
|
-
new GCMParameterSpec(128, FIXED_IV)
|
|
367
|
-
);
|
|
368
|
-
byte[] decryptedData = cipher.doFinal(encryptedData);
|
|
369
|
-
return new String(decryptedData, StandardCharsets.UTF_8);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
@SuppressLint("NewAPI") // API level is already checked
|
|
373
|
-
private Key generateKey(String KEY_ALIAS)
|
|
374
|
-
throws GeneralSecurityException, IOException {
|
|
375
|
-
Key key;
|
|
376
|
-
try {
|
|
377
|
-
key = generateKey(KEY_ALIAS, true);
|
|
378
|
-
} catch (StrongBoxUnavailableException e) {
|
|
379
|
-
key = generateKey(KEY_ALIAS, false);
|
|
353
|
+
|
|
354
|
+
private KeyStore getKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
|
|
355
|
+
if (keyStore == null) {
|
|
356
|
+
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
|
|
357
|
+
keyStore.load(null);
|
|
358
|
+
}
|
|
359
|
+
return keyStore;
|
|
380
360
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
Build.VERSION.SDK_INT > 34
|
|
402
|
-
) {
|
|
403
|
-
// Avoiding setUnlockedDeviceRequired(true) due to known issues on Android 12-14
|
|
404
|
-
paramBuilder.setUnlockedDeviceRequired(true);
|
|
405
|
-
}
|
|
406
|
-
paramBuilder.setIsStrongBoxBacked(isStrongBoxBacked);
|
|
361
|
+
|
|
362
|
+
private Key getAESKey(String KEY_ALIAS)
|
|
363
|
+
throws CertificateException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, KeyStoreException, NoSuchProviderException, UnrecoverableEntryException, IOException, InvalidAlgorithmParameterException {
|
|
364
|
+
SharedPreferences sharedPreferences = getContext().getSharedPreferences("", Context.MODE_PRIVATE);
|
|
365
|
+
String encryptedKeyB64 = sharedPreferences.getString(ENCRYPTED_KEY, null);
|
|
366
|
+
if (encryptedKeyB64 == null) {
|
|
367
|
+
byte[] key = new byte[16];
|
|
368
|
+
SecureRandom secureRandom = new SecureRandom();
|
|
369
|
+
secureRandom.nextBytes(key);
|
|
370
|
+
byte[] encryptedKey = rsaEncrypt(key, KEY_ALIAS);
|
|
371
|
+
encryptedKeyB64 = Base64.encodeToString(encryptedKey, Base64.DEFAULT);
|
|
372
|
+
SharedPreferences.Editor edit = sharedPreferences.edit();
|
|
373
|
+
edit.putString(ENCRYPTED_KEY, encryptedKeyB64);
|
|
374
|
+
edit.apply();
|
|
375
|
+
return new SecretKeySpec(key, "AES");
|
|
376
|
+
} else {
|
|
377
|
+
byte[] encryptedKey = Base64.decode(encryptedKeyB64, Base64.DEFAULT);
|
|
378
|
+
byte[] key = rsaDecrypt(encryptedKey, KEY_ALIAS);
|
|
379
|
+
return new SecretKeySpec(key, "AES");
|
|
380
|
+
}
|
|
407
381
|
}
|
|
408
382
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
383
|
+
private KeyStore.PrivateKeyEntry getPrivateKeyEntry(String KEY_ALIAS)
|
|
384
|
+
throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, CertificateException, KeyStoreException, IOException, UnrecoverableEntryException {
|
|
385
|
+
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) getKeyStore().getEntry(KEY_ALIAS, null);
|
|
386
|
+
|
|
387
|
+
if (privateKeyEntry == null) {
|
|
388
|
+
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE);
|
|
389
|
+
keyPairGenerator.initialize(
|
|
390
|
+
new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
|
391
|
+
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
|
|
392
|
+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
|
|
393
|
+
.setUserAuthenticationRequired(true)
|
|
394
|
+
// Set authentication validity duration to 0 to require authentication for every use
|
|
395
|
+
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
|
|
396
|
+
.build()
|
|
397
|
+
);
|
|
398
|
+
keyPairGenerator.generateKeyPair();
|
|
399
|
+
// Get the newly generated key entry
|
|
400
|
+
privateKeyEntry = (KeyStore.PrivateKeyEntry) getKeyStore().getEntry(KEY_ALIAS, null);
|
|
401
|
+
}
|
|
412
402
|
|
|
413
|
-
|
|
414
|
-
throws GeneralSecurityException, IOException {
|
|
415
|
-
KeyStore.SecretKeyEntry secretKeyEntry =
|
|
416
|
-
(KeyStore.SecretKeyEntry) getKeyStore().getEntry(KEY_ALIAS, null);
|
|
417
|
-
if (secretKeyEntry != null) {
|
|
418
|
-
return secretKeyEntry.getSecretKey();
|
|
403
|
+
return privateKeyEntry;
|
|
419
404
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
SharedPreferences sharedPreferences = getContext()
|
|
435
|
-
.getSharedPreferences("", Context.MODE_PRIVATE);
|
|
436
|
-
String encryptedKeyB64 = sharedPreferences.getString(ENCRYPTED_KEY, null);
|
|
437
|
-
if (encryptedKeyB64 == null) {
|
|
438
|
-
byte[] key = new byte[16];
|
|
439
|
-
SecureRandom secureRandom = new SecureRandom();
|
|
440
|
-
secureRandom.nextBytes(key);
|
|
441
|
-
byte[] encryptedKey = rsaEncrypt(key, KEY_ALIAS);
|
|
442
|
-
encryptedKeyB64 = Base64.encodeToString(encryptedKey, Base64.DEFAULT);
|
|
443
|
-
SharedPreferences.Editor edit = sharedPreferences.edit();
|
|
444
|
-
edit.putString(ENCRYPTED_KEY, encryptedKeyB64);
|
|
445
|
-
edit.apply();
|
|
446
|
-
return new SecretKeySpec(key, "AES");
|
|
447
|
-
} else {
|
|
448
|
-
byte[] encryptedKey = Base64.decode(encryptedKeyB64, Base64.DEFAULT);
|
|
449
|
-
byte[] key = rsaDecrypt(encryptedKey, KEY_ALIAS);
|
|
450
|
-
return new SecretKeySpec(key, "AES");
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
private KeyStore.PrivateKeyEntry getPrivateKeyEntry(String KEY_ALIAS)
|
|
455
|
-
throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, CertificateException, KeyStoreException, IOException, UnrecoverableEntryException {
|
|
456
|
-
KeyStore.PrivateKeyEntry privateKeyEntry =
|
|
457
|
-
(KeyStore.PrivateKeyEntry) getKeyStore().getEntry(KEY_ALIAS, null);
|
|
458
|
-
|
|
459
|
-
if (privateKeyEntry == null) {
|
|
460
|
-
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
|
|
461
|
-
KeyProperties.KEY_ALGORITHM_RSA,
|
|
462
|
-
ANDROID_KEY_STORE
|
|
463
|
-
);
|
|
464
|
-
keyPairGenerator.initialize(
|
|
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)
|
|
471
|
-
.setUserAuthenticationRequired(true)
|
|
472
|
-
// Set authentication validity duration to 0 to require authentication for every use
|
|
473
|
-
.setUserAuthenticationParameters(
|
|
474
|
-
0,
|
|
475
|
-
KeyProperties.AUTH_BIOMETRIC_STRONG
|
|
476
|
-
)
|
|
477
|
-
.build()
|
|
478
|
-
);
|
|
479
|
-
keyPairGenerator.generateKeyPair();
|
|
480
|
-
// Get the newly generated key entry
|
|
481
|
-
privateKeyEntry = (KeyStore.PrivateKeyEntry) getKeyStore()
|
|
482
|
-
.getEntry(KEY_ALIAS, null);
|
|
405
|
+
|
|
406
|
+
private byte[] rsaEncrypt(byte[] secret, String KEY_ALIAS)
|
|
407
|
+
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableEntryException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
|
408
|
+
KeyStore.PrivateKeyEntry privateKeyEntry = getPrivateKeyEntry(KEY_ALIAS);
|
|
409
|
+
// Encrypt the text
|
|
410
|
+
Cipher inputCipher = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
|
|
411
|
+
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.getCertificate().getPublicKey());
|
|
412
|
+
|
|
413
|
+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
414
|
+
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, inputCipher);
|
|
415
|
+
cipherOutputStream.write(secret);
|
|
416
|
+
cipherOutputStream.close();
|
|
417
|
+
|
|
418
|
+
return outputStream.toByteArray();
|
|
483
419
|
}
|
|
484
420
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
);
|
|
503
|
-
cipherOutputStream.write(secret);
|
|
504
|
-
cipherOutputStream.close();
|
|
505
|
-
|
|
506
|
-
return outputStream.toByteArray();
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
private byte[] rsaDecrypt(byte[] encrypted, String KEY_ALIAS)
|
|
510
|
-
throws UnrecoverableEntryException, NoSuchAlgorithmException, KeyStoreException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, IOException, CertificateException, InvalidAlgorithmParameterException {
|
|
511
|
-
KeyStore.PrivateKeyEntry privateKeyEntry = getPrivateKeyEntry(KEY_ALIAS);
|
|
512
|
-
Cipher output = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
|
|
513
|
-
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey());
|
|
514
|
-
CipherInputStream cipherInputStream = new CipherInputStream(
|
|
515
|
-
new ByteArrayInputStream(encrypted),
|
|
516
|
-
output
|
|
517
|
-
);
|
|
518
|
-
ArrayList<Byte> values = new ArrayList<>();
|
|
519
|
-
int nextByte;
|
|
520
|
-
while ((nextByte = cipherInputStream.read()) != -1) {
|
|
521
|
-
values.add((byte) nextByte);
|
|
421
|
+
private byte[] rsaDecrypt(byte[] encrypted, String KEY_ALIAS)
|
|
422
|
+
throws UnrecoverableEntryException, NoSuchAlgorithmException, KeyStoreException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, IOException, CertificateException, InvalidAlgorithmParameterException {
|
|
423
|
+
KeyStore.PrivateKeyEntry privateKeyEntry = getPrivateKeyEntry(KEY_ALIAS);
|
|
424
|
+
Cipher output = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
|
|
425
|
+
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey());
|
|
426
|
+
CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(encrypted), output);
|
|
427
|
+
ArrayList<Byte> values = new ArrayList<>();
|
|
428
|
+
int nextByte;
|
|
429
|
+
while ((nextByte = cipherInputStream.read()) != -1) {
|
|
430
|
+
values.add((byte) nextByte);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
byte[] bytes = new byte[values.size()];
|
|
434
|
+
for (int i = 0; i < bytes.length; i++) {
|
|
435
|
+
bytes[i] = values.get(i);
|
|
436
|
+
}
|
|
437
|
+
return bytes;
|
|
522
438
|
}
|
|
523
439
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
440
|
+
private boolean deviceHasCredentials() {
|
|
441
|
+
KeyguardManager keyguardManager = (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
|
|
442
|
+
// Can only use fallback if the device has a pin/pattern/password lockscreen.
|
|
443
|
+
return keyguardManager.isDeviceSecure();
|
|
527
444
|
}
|
|
528
|
-
return bytes;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
private boolean deviceHasCredentials() {
|
|
532
|
-
KeyguardManager keyguardManager = (KeyguardManager) getActivity()
|
|
533
|
-
.getSystemService(Context.KEYGUARD_SERVICE);
|
|
534
|
-
// Can only use fallback if the device has a pin/pattern/password lockscreen.
|
|
535
|
-
return keyguardManager.isDeviceSecure();
|
|
536
|
-
}
|
|
537
445
|
}
|