· 5 years ago · Aug 04, 2020, 06:58 PM
1package br.com.classapp.RNSensitiveInfo;
2
3import android.app.Activity;
4import android.app.KeyguardManager;
5import android.content.Context;
6import android.content.SharedPreferences;
7import android.hardware.fingerprint.FingerprintManager;
8import android.os.Build;
9import android.os.CancellationSignal;
10import android.security.keystore.KeyGenParameterSpec;
11import android.security.keystore.KeyInfo;
12
13import java.security.InvalidKeyException;
14
15import android.security.keystore.KeyProperties;
16import android.util.Base64;
17import android.util.Log;
18
19import androidx.annotation.NonNull;
20import androidx.biometric.BiometricConstants;
21import androidx.biometric.BiometricManager;
22import androidx.biometric.BiometricPrompt;
23
24import com.facebook.react.bridge.Promise;
25import com.facebook.react.bridge.ReactApplicationContext;
26import com.facebook.react.bridge.ReactContextBaseJavaModule;
27import com.facebook.react.bridge.ReactMethod;
28import com.facebook.react.bridge.ReadableMap;
29import com.facebook.react.bridge.WritableMap;
30import com.facebook.react.bridge.WritableNativeMap;
31import com.facebook.react.bridge.UiThreadUtil;
32import com.facebook.react.modules.core.DeviceEventManagerModule;
33
34import java.security.KeyStore;
35import java.security.UnrecoverableKeyException;
36import java.util.HashMap;
37import java.util.Map;
38import java.util.concurrent.Executor;
39import java.util.concurrent.Executors;
40
41import javax.crypto.Cipher;
42import javax.crypto.IllegalBlockSizeException;
43import javax.crypto.KeyGenerator;
44import javax.crypto.SecretKey;
45import javax.crypto.SecretKeyFactory;
46import javax.crypto.spec.IvParameterSpec;
47
48import androidx.fragment.app.FragmentActivity;
49import br.com.classapp.RNSensitiveInfo.utils.AppConstants;
50
51public class RNSensitiveInfoModule extends ReactContextBaseJavaModule {
52
53 // This must have 'AndroidKeyStore' as value. Unfortunately there is no predefined constant.
54 private static final String ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore";
55
56 // This is the default transformation used throughout this sample project.
57 private static final String AES_DEFAULT_TRANSFORMATION =
58 KeyProperties.KEY_ALGORITHM_AES + "/" +
59 KeyProperties.BLOCK_MODE_CBC + "/" +
60 KeyProperties.ENCRYPTION_PADDING_PKCS7;
61
62 private static final String KEY_ALIAS_AES = "MyAesKeyAlias";
63 private static final String DELIMITER = "]";
64
65 private FingerprintManager mFingerprintManager;
66 private KeyStore mKeyStore;
67 private CancellationSignal mCancellationSignal;
68
69 // Keep it true by default to maintain backwards compatibility with existing users.
70 private boolean invalidateEnrollment = true;
71
72 public RNSensitiveInfoModule(ReactApplicationContext reactContext) {
73 super(reactContext);
74 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
75 try {
76 mFingerprintManager = (FingerprintManager) reactContext.getSystemService(Context.FINGERPRINT_SERVICE);
77 } catch (Exception e) {
78 Log.d("RNSensitiveInfo", "Fingerprint not supported");
79 }
80 initKeyStore();
81 }
82 }
83
84 @Override
85 public String getName() {
86 return "RNSensitiveInfo";
87 }
88
89 /**
90 * Checks whether the device supports Biometric authentication and if the user has
91 * enrolled at least one credential.
92 *
93 * @return true if the user has a biometric capable device and has enrolled
94 * one or more credentials
95 */
96 private boolean hasSetupBiometricCredential() {
97 try {
98 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
99 ReactApplicationContext reactApplicationContext = getReactApplicationContext();
100 BiometricManager biometricManager = BiometricManager.from(reactApplicationContext);
101 int canAuthenticate = biometricManager.canAuthenticate();
102
103 return canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS;
104 } else {
105 return false;
106 }
107 } catch (Exception e) {
108 return false;
109 }
110 }
111
112 private boolean isKeyguardSecure() {
113 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
114 ReactApplicationContext reactApplicationContext = getReactApplicationContext();
115 KeyguardManager keyguardManager = (KeyguardManager) reactApplicationContext.getSystemService(Context.KEYGUARD_SERVICE);
116
117 return keyguardManager.isKeyguardSecure();
118 } else {
119 return false;
120 }
121 }
122
123 private boolean isDeviceSecure() {
124 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
125 ReactApplicationContext reactApplicationContext = getReactApplicationContext();
126 KeyguardManager keyguardManager = (KeyguardManager) reactApplicationContext.getSystemService(Context.KEYGUARD_SERVICE);
127
128 return keyguardManager.isDeviceSecure();
129 } else {
130 return false;
131 }
132 }
133
134 @ReactMethod
135 public void setInvalidatedByBiometricEnrollment(final boolean invalidatedByBiometricEnrollment, final Promise pm) {
136 this.invalidateEnrollment = invalidatedByBiometricEnrollment;
137 try {
138 prepareKey();
139 } catch (Exception e) {
140 pm.reject(e);
141 }
142 }
143
144 @ReactMethod
145 public void isHardwareDetected(final Promise pm) {
146 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
147 ReactApplicationContext reactApplicationContext = getReactApplicationContext();
148 BiometricManager biometricManager = BiometricManager.from(reactApplicationContext);
149 int canAuthenticate = biometricManager.canAuthenticate();
150
151 pm.resolve(canAuthenticate != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
152 } else {
153 pm.resolve(false);
154 }
155 }
156
157 @ReactMethod
158 public void hasEnrolledFingerprints(final Promise pm) {
159 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mFingerprintManager != null) {
160 pm.resolve(mFingerprintManager.hasEnrolledFingerprints());
161 } else {
162 pm.resolve(false);
163 }
164 }
165
166 @ReactMethod
167 public void isSensorAvailable(final Promise promise) {
168 try {
169 boolean biometricAvailable = hasSetupBiometricCredential();
170 boolean isDeviceSecure = isDeviceSecure();
171 boolean isKeyguardSecure = isKeyguardSecure();
172
173 promise.resolve(biometricAvailable && isDeviceSecure && isKeyguardSecure);
174 } catch (Exception e) {
175 promise.reject(e);
176 }
177 }
178
179 @ReactMethod
180 public void getItem(String key, ReadableMap options, Promise pm) {
181
182 String name = sharedPreferences(options);
183
184 String value = prefs(name).getString(key, null);
185
186 if (value != null && options.hasKey("touchID") && options.getBoolean("touchID")) {
187 boolean showModal = options.hasKey("showModal") && options.getBoolean("showModal");
188 HashMap strings = options.hasKey("strings") ? options.getMap("strings").toHashMap() : new HashMap();
189
190 decryptWithAes(value, showModal, strings, pm, null);
191 } else {
192 pm.resolve(value);
193 }
194 }
195
196 @ReactMethod
197 public void setItem(String key, String value, ReadableMap options, Promise pm) {
198
199 String name = sharedPreferences(options);
200
201 if (options.hasKey("touchID") && options.getBoolean("touchID")) {
202 boolean showModal = options.hasKey("showModal") && options.getBoolean("showModal");
203 HashMap strings = options.hasKey("strings") ? options.getMap("strings").toHashMap() : new HashMap();
204
205 putExtraWithAES(key, value, prefs(name), showModal, strings, pm, null);
206 } else {
207 try {
208 putExtra(key, value, prefs(name));
209 pm.resolve(value);
210 } catch (Exception e) {
211 Log.d("RNSensitiveInfo", e.getCause().getMessage());
212 pm.reject(e);
213 }
214 }
215 }
216
217
218 @ReactMethod
219 public void deleteItem(String key, ReadableMap options, Promise pm) {
220
221 String name = sharedPreferences(options);
222
223 SharedPreferences.Editor editor = prefs(name).edit();
224
225 editor.remove(key).apply();
226
227 pm.resolve(null);
228 }
229
230
231 @ReactMethod
232 public void getAllItems(ReadableMap options, Promise pm) {
233
234 String name = sharedPreferences(options);
235
236 Map<String, ?> allEntries = prefs(name).getAll();
237 WritableMap resultData = new WritableNativeMap();
238
239 for (Map.Entry<String, ?> entry : allEntries.entrySet()) {
240 String value = entry.getValue().toString();
241 resultData.putString(entry.getKey(), value);
242 }
243 pm.resolve(resultData);
244 }
245
246 @ReactMethod
247 public void cancelFingerprintAuth() {
248 if (mCancellationSignal != null && !mCancellationSignal.isCanceled()) {
249 mCancellationSignal.cancel();
250 }
251 }
252
253 private SharedPreferences prefs(String name) {
254 return getReactApplicationContext().getSharedPreferences(name, Context.MODE_PRIVATE);
255 }
256
257 @NonNull
258 private String sharedPreferences(ReadableMap options) {
259 String name = options.hasKey("sharedPreferencesName") ? options.getString("sharedPreferencesName") : "shared_preferences";
260 if (name == null) {
261 name = "shared_preferences";
262 }
263 return name;
264 }
265
266
267 private void putExtra(String key, Object value, SharedPreferences mSharedPreferences) {
268 SharedPreferences.Editor editor = mSharedPreferences.edit();
269 if (value instanceof String) {
270 editor.putString(key, (String) value).apply();
271 } else if (value instanceof Boolean) {
272 editor.putBoolean(key, (Boolean) value).apply();
273 } else if (value instanceof Integer) {
274 editor.putInt(key, (Integer) value).apply();
275 } else if (value instanceof Long) {
276 editor.putLong(key, (Long) value).apply();
277 } else if (value instanceof Float) {
278 editor.putFloat(key, (Float) value).apply();
279 }
280 }
281
282 private void showDialog(final HashMap strings, final BiometricPrompt.CryptoObject cryptoObject, final BiometricPrompt.AuthenticationCallback callback) {
283 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
284
285 UiThreadUtil.runOnUiThread(
286 new Runnable() {
287 @Override
288 public void run() {
289 try {
290 Activity activity = getCurrentActivity();
291 if (activity == null) {
292 callback.onAuthenticationError(BiometricConstants.ERROR_CANCELED,
293 strings.containsKey("cancelled") ? strings.get("cancelled").toString() : "Authentication was cancelled");
294 return;
295 }
296
297 FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity();
298 Executor executor = Executors.newSingleThreadExecutor();
299 BiometricPrompt biometricPrompt = new BiometricPrompt(fragmentActivity, executor, callback);
300
301 BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
302 .setDeviceCredentialAllowed(false)
303 .setNegativeButtonText(strings.containsKey("cancel") ? strings.get("cancel").toString() : "Cancel")
304 .setDescription(strings.containsKey("description") ? strings.get("description").toString() : null)
305 .setTitle(strings.containsKey("header") ? strings.get("header").toString() : null)
306 .build();
307 biometricPrompt.authenticate(promptInfo, cryptoObject);
308 } catch (Exception e) {
309 throw e;
310 }
311 }
312 }
313 );
314 }
315 }
316
317 /**
318 * Generates a new AES key and stores it under the { @code KEY_ALIAS_AES } in the
319 * Android Keystore.
320 */
321 private void initKeyStore() {
322 try {
323 mKeyStore = KeyStore.getInstance(ANDROID_KEYSTORE_PROVIDER);
324 mKeyStore.load(null);
325
326 // Check if a generated key exists under the KEY_ALIAS_AES .
327 if (!mKeyStore.containsAlias(KEY_ALIAS_AES)) {
328 prepareKey();
329 }
330 } catch (Exception e) {
331 //
332 }
333 }
334
335 private void prepareKey() throws Exception {
336 if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
337 return;
338 }
339 KeyGenerator keyGenerator = KeyGenerator.getInstance(
340 KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE_PROVIDER);
341
342 KeyGenParameterSpec.Builder builder = null;
343 builder = new KeyGenParameterSpec.Builder(
344 KEY_ALIAS_AES,
345 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT);
346
347 builder.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
348 .setKeySize(256)
349 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
350 // forces user authentication with fingerprint
351 .setUserAuthenticationRequired(true);
352
353 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
354 try {
355 builder.setInvalidatedByBiometricEnrollment(invalidateEnrollment);
356 } catch (Exception e) {
357 Log.d("RNSensitiveInfo", "Error setting setInvalidatedByBiometricEnrollment: " + e.getMessage());
358 }
359 }
360
361 keyGenerator.init(builder.build());
362 keyGenerator.generateKey();
363 }
364
365 private void putExtraWithAES(final String key, final String value, final SharedPreferences mSharedPreferences, final boolean showModal, final HashMap strings, final Promise pm, Cipher cipher) {
366
367 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M && hasSetupBiometricCredential()) {
368 try {
369 if (cipher == null) {
370 SecretKey secretKey = (SecretKey) mKeyStore.getKey(KEY_ALIAS_AES, null);
371 cipher = Cipher.getInstance(AES_DEFAULT_TRANSFORMATION);
372 cipher.init(Cipher.ENCRYPT_MODE, secretKey);
373
374 // Retrieve information about the SecretKey from the KeyStore.
375 SecretKeyFactory factory = SecretKeyFactory.getInstance(
376 secretKey.getAlgorithm(), ANDROID_KEYSTORE_PROVIDER);
377 KeyInfo info = (KeyInfo) factory.getKeySpec(secretKey, KeyInfo.class);
378
379 if (info.isUserAuthenticationRequired() &&
380 info.getUserAuthenticationValidityDurationSeconds() == -1) {
381
382 if (showModal) {
383 class PutExtraWithAESCallback extends BiometricPrompt.AuthenticationCallback {
384 @Override
385 public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
386 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
387 putExtraWithAES(key, value, mSharedPreferences, true, strings, pm, result.getCryptoObject().getCipher());
388 }
389 }
390
391 @Override
392 public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
393 pm.reject(String.valueOf(errorCode), errString.toString());
394 }
395
396 @Override
397 public void onAuthenticationFailed() {
398 getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
399 .emit(AppConstants.E_AUTHENTICATION_NOT_RECOGNIZED, "Authentication not recognized.");
400 }
401 }
402
403 showDialog(strings, new BiometricPrompt.CryptoObject(cipher), new PutExtraWithAESCallback());
404 } else {
405 mCancellationSignal = new CancellationSignal();
406 mFingerprintManager.authenticate(new FingerprintManager.CryptoObject(cipher), mCancellationSignal,
407 0, new FingerprintManager.AuthenticationCallback() {
408
409 @Override
410 public void onAuthenticationFailed() {
411 super.onAuthenticationFailed();
412 getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
413 .emit(AppConstants.E_AUTHENTICATION_NOT_RECOGNIZED, "Fingerprint not recognized.");
414 }
415
416 @Override
417 public void onAuthenticationError(int errorCode, CharSequence errString) {
418 super.onAuthenticationError(errorCode, errString);
419 pm.reject(String.valueOf(errorCode), errString.toString());
420 }
421
422 @Override
423 public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
424 super.onAuthenticationHelp(helpCode, helpString);
425 getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
426 .emit(AppConstants.FINGERPRINT_AUTHENTICATION_HELP, helpString.toString());
427 }
428
429 @Override
430 public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
431 super.onAuthenticationSucceeded(result);
432 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
433 putExtraWithAES(key, value, mSharedPreferences, false, strings, pm, result.getCryptoObject().getCipher());
434 }
435 }
436 }, null);
437 }
438 }
439 return;
440 }
441
442 byte[] encryptedBytes = cipher.doFinal(value.getBytes());
443
444 // Encode the initialization vector (IV) and encryptedBytes to Base64.
445 String base64IV = Base64.encodeToString(cipher.getIV(), Base64.DEFAULT);
446 String base64Cipher = Base64.encodeToString(encryptedBytes, Base64.DEFAULT);
447
448 String result = base64IV + DELIMITER + base64Cipher;
449
450 putExtra(key, result, mSharedPreferences);
451 pm.resolve(value);
452 } catch (InvalidKeyException | UnrecoverableKeyException e) {
453 try {
454 mKeyStore.deleteEntry(KEY_ALIAS_AES);
455 prepareKey();
456 } catch (Exception keyResetError) {
457 pm.reject(keyResetError);
458 }
459 pm.reject(e);
460 } catch (IllegalBlockSizeException e){
461 if(e.getCause() != null && e.getCause().getMessage().contains("Key user not authenticated")) {
462 try {
463 mKeyStore.deleteEntry(KEY_ALIAS_AES);
464 prepareKey();
465 pm.reject(AppConstants.KM_ERROR_KEY_USER_NOT_AUTHENTICATED, e.getCause().getMessage());
466 } catch (Exception keyResetError) {
467 pm.reject(keyResetError);
468 }
469 } else {
470 pm.reject(e);
471 }
472 } catch (SecurityException e) {
473 pm.reject(e);
474 } catch (Exception e) {
475 pm.reject(e);
476 }
477 } else {
478 pm.reject(AppConstants.E_BIOMETRIC_NOT_SUPPORTED, "Biometrics not supported");
479 }
480 }
481
482 private void decryptWithAes(final String encrypted, final boolean showModal, final HashMap strings, final Promise pm, Cipher cipher) {
483
484 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M
485 && hasSetupBiometricCredential()) {
486
487 String[] inputs = encrypted.split(DELIMITER);
488 if (inputs.length < 2) {
489 pm.reject("DecryptionFailed", "DecryptionFailed");
490 }
491
492 try {
493 byte[] iv = Base64.decode(inputs[0], Base64.DEFAULT);
494 byte[] cipherBytes = Base64.decode(inputs[1], Base64.DEFAULT);
495
496 if (cipher == null) {
497 SecretKey secretKey = (SecretKey) mKeyStore.getKey(KEY_ALIAS_AES, null);
498 cipher = Cipher.getInstance(AES_DEFAULT_TRANSFORMATION);
499 cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
500
501 SecretKeyFactory factory = SecretKeyFactory.getInstance(
502 secretKey.getAlgorithm(), ANDROID_KEYSTORE_PROVIDER);
503 KeyInfo info = (KeyInfo) factory.getKeySpec(secretKey, KeyInfo.class);
504
505 if (info.isUserAuthenticationRequired() &&
506 info.getUserAuthenticationValidityDurationSeconds() == -1) {
507
508 if (showModal) {
509 class DecryptWithAesCallback extends BiometricPrompt.AuthenticationCallback {
510 @Override
511 public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
512 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
513 decryptWithAes(encrypted, true, strings, pm, result.getCryptoObject().getCipher());
514 }
515 }
516
517 @Override
518 public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
519 pm.reject(String.valueOf(errorCode), errString.toString());
520 }
521
522 @Override
523 public void onAuthenticationFailed() {
524 getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
525 .emit(AppConstants.E_AUTHENTICATION_NOT_RECOGNIZED, "Authentication not recognized.");
526 }
527 }
528
529 showDialog(strings, new BiometricPrompt.CryptoObject(cipher), new DecryptWithAesCallback());
530 } else {
531 mCancellationSignal = new CancellationSignal();
532 mFingerprintManager.authenticate(new FingerprintManager.CryptoObject(cipher), mCancellationSignal,
533 0, new FingerprintManager.AuthenticationCallback() {
534
535 @Override
536 public void onAuthenticationFailed() {
537 super.onAuthenticationFailed();
538 getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
539 .emit(AppConstants.E_AUTHENTICATION_NOT_RECOGNIZED, "Fingerprint not recognized.");
540 }
541
542 @Override
543 public void onAuthenticationError(int errorCode, CharSequence errString) {
544 super.onAuthenticationError(errorCode, errString);
545 pm.reject(String.valueOf(errorCode), errString.toString());
546 }
547
548 @Override
549 public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
550 super.onAuthenticationHelp(helpCode, helpString);
551 getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
552 .emit(AppConstants.FINGERPRINT_AUTHENTICATION_HELP, helpString.toString());
553 }
554
555 @Override
556 public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
557 super.onAuthenticationSucceeded(result);
558 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
559 decryptWithAes(encrypted, false, strings, pm, result.getCryptoObject().getCipher());
560 }
561 }
562 }, null);
563 }
564 }
565 return;
566 }
567 byte[] decryptedBytes = cipher.doFinal(cipherBytes);
568 pm.resolve(new String(decryptedBytes));
569 } catch (InvalidKeyException | UnrecoverableKeyException e) {
570 try {
571 mKeyStore.deleteEntry(KEY_ALIAS_AES);
572 prepareKey();
573 } catch (Exception keyResetError) {
574 pm.reject(keyResetError);
575 }
576 pm.reject(e);
577 } catch (IllegalBlockSizeException e){
578 if(e.getCause() != null && e.getCause().getMessage().contains("Key user not authenticated")) {
579 try {
580 mKeyStore.deleteEntry(KEY_ALIAS_AES);
581 prepareKey();
582 pm.reject(AppConstants.KM_ERROR_KEY_USER_NOT_AUTHENTICATED, e.getCause().getMessage());
583 } catch (Exception keyResetError) {
584 pm.reject(keyResetError);
585 }
586 } else {
587 pm.reject(e);
588 }
589 } catch (SecurityException e) {
590 pm.reject(e);
591 } catch (Exception e) {
592 pm.reject(e);
593 }
594 } else {
595 pm.reject(AppConstants.E_BIOMETRIC_NOT_SUPPORTED, "Biometrics not supported");
596 }
597 }
598}
599