@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.
Files changed (35) hide show
  1. package/Package.swift +28 -0
  2. package/README.md +4 -0
  3. package/android/src/main/java/ee/forgr/biometric/AuthActivity.java +141 -166
  4. package/android/src/main/java/ee/forgr/biometric/NativeBiometric.java +351 -443
  5. package/dist/esm/index.d.ts +2 -2
  6. package/dist/esm/index.js +4 -4
  7. package/dist/esm/index.js.map +1 -1
  8. package/dist/esm/web.d.ts +2 -2
  9. package/dist/esm/web.js +10 -10
  10. package/dist/esm/web.js.map +1 -1
  11. package/dist/plugin.cjs.js +10 -10
  12. package/dist/plugin.cjs.js.map +1 -1
  13. package/dist/plugin.js +10 -10
  14. package/dist/plugin.js.map +1 -1
  15. package/ios/{Plugin/Plugin.swift → Sources/CapgoNativeBiometricPlugin/CapgoNativeBiometricPlugin.swift} +3 -3
  16. package/ios/Tests/CapgoNativeBiometricPluginTests/CapgoNativeBiometricPluginTests.swift +13 -0
  17. package/package.json +20 -19
  18. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  19. package/android/gradle/wrapper/gradle-wrapper.properties +0 -7
  20. package/android/gradle.properties +0 -20
  21. package/android/gradlew +0 -252
  22. package/android/gradlew.bat +0 -94
  23. package/android/proguard-rules.pro +0 -21
  24. package/android/settings.gradle +0 -2
  25. package/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +0 -27
  26. package/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +0 -18
  27. package/ios/Plugin/Info.plist +0 -24
  28. package/ios/Plugin.xcodeproj/project.pbxproj +0 -546
  29. package/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  30. package/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  31. package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
  32. package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  33. package/ios/PluginTests/Info.plist +0 -22
  34. package/ios/PluginTests/PluginTests.swift +0 -25
  35. 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
- //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 =
70
- "NativeBiometricSharedPreferences";
71
-
72
- private SharedPreferences encryptedSharedPreferences;
73
-
74
- private int getAvailableFeature() {
75
- // default to none
76
- BiometricManager biometricManager = BiometricManager.from(getContext());
77
-
78
- // Check for biometric capabilities
79
- int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
80
- int canAuthenticate = biometricManager.canAuthenticate(authenticators);
81
-
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
- }
92
-
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
- }
111
-
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
- }
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
- return NONE;
125
- }
119
+ @PluginMethod
120
+ public void isAvailable(PluginCall call) {
121
+ JSObject ret = new JSObject();
126
122
 
127
- @PluginMethod
128
- public void isAvailable(PluginCall call) {
129
- JSObject ret = new JSObject();
123
+ boolean useFallback = Boolean.TRUE.equals(call.getBoolean("useFallback", false));
130
124
 
131
- boolean useFallback = Boolean.TRUE.equals(
132
- call.getBoolean("useFallback", false)
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
- BiometricManager biometricManager = BiometricManager.from(getContext());
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
- );
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
- boolean isAvailable =
152
- (canAuthenticateResult == BiometricManager.BIOMETRIC_SUCCESS ||
153
- fallbackAvailable);
154
- ret.put("isAvailable", isAvailable);
155
-
156
- if (!isAvailable) {
157
- // BiometricManager Error Constants use the same values as BiometricPrompt's Constants. So we can reuse our
158
- int pluginErrorCode = AuthActivity.convertToPluginErrorCode(
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
- ret.put("biometryType", getAvailableFeature());
165
- call.resolve(ret);
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
- @PluginMethod
169
- public void verifyIdentity(final PluginCall call) throws JSONException {
170
- Intent intent = new Intent(getContext(), AuthActivity.class);
163
+ String description = call.getString("description");
164
+ if (description != null) {
165
+ intent.putExtra("description", description);
166
+ }
171
167
 
172
- intent.putExtra("title", call.getString("title", "Authenticate"));
168
+ String negativeButtonText = call.getString("negativeButtonText");
169
+ if (negativeButtonText != null) {
170
+ intent.putExtra("negativeButtonText", negativeButtonText);
171
+ }
173
172
 
174
- String subtitle = call.getString("subtitle");
175
- if (subtitle != null) {
176
- intent.putExtra("subtitle", subtitle);
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
- String description = call.getString("description");
180
- if (description != null) {
181
- intent.putExtra("description", description);
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
- String negativeButtonText = call.getString("negativeButtonText");
185
- if (negativeButtonText != null) {
186
- intent.putExtra("negativeButtonText", negativeButtonText);
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
- Integer maxAttempts = call.getInt("maxAttempts");
190
- if (maxAttempts != null) {
191
- intent.putExtra("maxAttempts", maxAttempts);
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
- // Pass allowed biometry types
195
- JSArray allowedTypes = call.getArray("allowedBiometryTypes");
196
- if (allowedTypes != null) {
197
- int[] types = new int[allowedTypes.length()];
198
- for (int i = 0; i < allowedTypes.length(); i++) {
199
- types[i] = (int) allowedTypes.toList().get(i);
200
- }
201
- intent.putExtra("allowedBiometryTypes", types);
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
- boolean useFallback = Boolean.TRUE.equals(
205
- call.getBoolean("useFallback", false)
206
- );
207
- if (useFallback) {
208
- useFallback = this.deviceHasCredentials();
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
- intent.putExtra("useFallback", useFallback);
212
-
213
- startActivityForResult(call, intent, "verifyResult");
214
- }
215
-
216
- @PluginMethod
217
- public void setCredentials(final PluginCall call) {
218
- String username = call.getString("username", null);
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
- @PluginMethod
250
- public void getCredentials(final PluginCall call) {
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
- JSObject jsObject = new JSObject();
270
- jsObject.put("username", decryptString(username, KEY_ALIAS));
271
- jsObject.put("password", decryptString(password, KEY_ALIAS));
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
- } else {
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
- @ActivityCallback
287
- private void verifyResult(PluginCall call, ActivityResult result) {
288
- if (result.getResultCode() == Activity.RESULT_OK) {
289
- Intent data = result.getData();
290
- if (data != null && data.hasExtra("result")) {
291
- switch (Objects.requireNonNull(data.getStringExtra("result"))) {
292
- case "success":
293
- call.resolve();
294
- break;
295
- case "failed":
296
- case "error":
297
- call.reject(
298
- data.getStringExtra("errorDetails"),
299
- data.getStringExtra("errorCode")
300
- );
301
- break;
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
- } else {
309
- call.reject("Something went wrong.");
341
+
342
+ generator.init(paramBuilder.build());
343
+ return generator.generateKey();
310
344
  }
311
- }
312
-
313
- @PluginMethod
314
- public void deleteCredentials(final PluginCall call) {
315
- String KEY_ALIAS = call.getString("server", null);
316
-
317
- if (KEY_ALIAS != null) {
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
- private String encryptString(String stringToEncrypt, String KEY_ALIAS)
343
- throws GeneralSecurityException, IOException {
344
- Cipher cipher;
345
- cipher = Cipher.getInstance(TRANSFORMATION);
346
- cipher.init(
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
- return key;
382
- }
383
-
384
- private Key generateKey(String KEY_ALIAS, boolean isStrongBoxBacked)
385
- throws GeneralSecurityException, IOException, StrongBoxUnavailableException {
386
- KeyGenerator generator = KeyGenerator.getInstance(
387
- KeyProperties.KEY_ALGORITHM_AES,
388
- ANDROID_KEY_STORE
389
- );
390
- KeyGenParameterSpec.Builder paramBuilder = new KeyGenParameterSpec.Builder(
391
- KEY_ALIAS,
392
- KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
393
- )
394
- .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
395
- .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
396
- .setRandomizedEncryptionRequired(false);
397
-
398
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
399
- if (
400
- Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
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
- generator.init(paramBuilder.build());
410
- return generator.generateKey();
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
- private Key getKey(String KEY_ALIAS)
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
- return generateKey(KEY_ALIAS);
421
- }
422
-
423
- private KeyStore getKeyStore()
424
- throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
425
- if (keyStore == null) {
426
- keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
427
- keyStore.load(null);
428
- }
429
- return keyStore;
430
- }
431
-
432
- private Key getAESKey(String KEY_ALIAS)
433
- throws CertificateException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, KeyStoreException, NoSuchProviderException, UnrecoverableEntryException, IOException, InvalidAlgorithmParameterException {
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
- return privateKeyEntry;
486
- }
487
-
488
- private byte[] rsaEncrypt(byte[] secret, String KEY_ALIAS)
489
- throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableEntryException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
490
- KeyStore.PrivateKeyEntry privateKeyEntry = getPrivateKeyEntry(KEY_ALIAS);
491
- // Encrypt the text
492
- Cipher inputCipher = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
493
- inputCipher.init(
494
- Cipher.ENCRYPT_MODE,
495
- privateKeyEntry.getCertificate().getPublicKey()
496
- );
497
-
498
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
499
- CipherOutputStream cipherOutputStream = new CipherOutputStream(
500
- outputStream,
501
- inputCipher
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
- byte[] bytes = new byte[values.size()];
525
- for (int i = 0; i < bytes.length; i++) {
526
- bytes[i] = values.get(i);
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
  }