11package com .bebnev .RNLocalAuthentication ;
22
3+ import android .os .Build ;
4+ import android .security .keystore .KeyGenParameterSpec ;
5+ import android .security .keystore .KeyPermanentlyInvalidatedException ;
6+ import android .security .keystore .KeyProperties ;
7+ import android .util .Log ;
8+
39import androidx .annotation .NonNull ;
10+ import androidx .annotation .RequiresApi ;
411import androidx .fragment .app .FragmentActivity ;
512import androidx .biometric .BiometricPrompt ;
613import androidx .biometric .BiometricManager ;
14+
715import static androidx .biometric .BiometricConstants .ERROR_NEGATIVE_BUTTON ;
816import static androidx .biometric .BiometricConstants .ERROR_USER_CANCELED ;
17+
18+ import java .io .IOException ;
19+ import java .nio .charset .Charset ;
20+ import java .security .InvalidAlgorithmParameterException ;
21+ import java .security .InvalidKeyException ;
22+ import java .security .KeyStore ;
23+ import java .security .KeyStoreException ;
24+ import java .security .NoSuchAlgorithmException ;
25+ import java .security .NoSuchProviderException ;
26+ import java .security .UnrecoverableKeyException ;
27+ import java .security .cert .CertificateException ;
28+ import java .util .Arrays ;
929import java .util .concurrent .Executor ;
1030
1131import com .facebook .react .bridge .ReadableMap ;
1838import com .facebook .react .bridge .WritableMap ;
1939import com .facebook .react .bridge .Arguments ;
2040
21- @ ReactModule (name =RNLocalAuthenticationModule .NAME )
41+ import javax .crypto .BadPaddingException ;
42+ import javax .crypto .Cipher ;
43+ import javax .crypto .IllegalBlockSizeException ;
44+ import javax .crypto .KeyGenerator ;
45+ import javax .crypto .NoSuchPaddingException ;
46+ import javax .crypto .SecretKey ;
47+
48+ @ ReactModule (name = RNLocalAuthenticationModule .NAME )
2249public class RNLocalAuthenticationModule extends ReactContextBaseJavaModule {
2350 public static final String NAME = "RNLocalAuthentication" ;
24-
25- private static final int AUTHORIZATION_FAILED = 9999 ;
51+ public static final String KEY_NAME = "biometric_example" ;
52+ private static final int AUTHORIZATION_FAILED = 9999 ;
53+ private static final int FINGERPRINT_CHANGE = 7777 ;
2654 private Executor executor = new MainThreadExecutor ();
2755 private BiometricPrompt biometricPrompt = null ;
2856
@@ -46,6 +74,7 @@ private BiometricManager getBiometricManager() {
4674
4775 /**
4876 * Check if scanner is supported on the device
77+ *
4978 * @param p
5079 */
5180 @ ReactMethod
@@ -95,11 +124,15 @@ public void getBiometryStatusAsync(final Promise p) {
95124 * @param options
96125 * @param p
97126 */
127+ @ RequiresApi (api = Build .VERSION_CODES .M )
98128 @ ReactMethod
99129 public void authenticateAsync (ReadableMap options , final Promise p ) {
100130 final boolean fallbackEnabled = options .hasKey ("fallbackEnabled" ) && !options .isNull ("fallbackEnabled" )
101- ? options .getBoolean ("fallbackEnabled" )
102- : false ;
131+ ? options .getBoolean ("fallbackEnabled" )
132+ : false ;
133+ final boolean isInit = options .hasKey ("isInit" ) && !options .isNull ("isInit" )
134+ ? options .getBoolean ("isInit" )
135+ : false ;
103136
104137 if (!options .hasKey ("reason" ) || options .isNull ("reason" )) {
105138 p .reject ("ReasonNotSet" , "Reason for requesting authentication is not specified" );
@@ -122,7 +155,7 @@ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString
122155 if (errorCode == ERROR_NEGATIVE_BUTTON && biometricPrompt != null ) {
123156 biometricPrompt .cancelAuthentication ();
124157 release ();
125- } else if (errorCode == ERROR_USER_CANCELED && !fallbackEnabled && biometricPrompt != null ) {
158+ } else if (errorCode == ERROR_USER_CANCELED && !fallbackEnabled && biometricPrompt != null ) {
126159 biometricPrompt .cancelAuthentication ();
127160 p .resolve (makeAuthorizationResponse (false , BiometricPrompt .ERROR_NEGATIVE_BUTTON ));
128161 release ();
@@ -132,10 +165,10 @@ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString
132165 p .resolve (makeAuthorizationResponse (false , errorCode ));
133166 }
134167
168+ @ RequiresApi (api = Build .VERSION_CODES .N )
135169 @ Override
136170 public void onAuthenticationSucceeded (@ NonNull BiometricPrompt .AuthenticationResult result ) {
137171 super .onAuthenticationSucceeded (result );
138-
139172 p .resolve (makeAuthorizationResponse (true , null ));
140173 release ();
141174 }
@@ -152,9 +185,26 @@ public void onAuthenticationFailed() {
152185
153186 UiThreadUtil .runOnUiThread (
154187 new Runnable () {
188+ @ RequiresApi (api = Build .VERSION_CODES .N )
155189 @ Override
156190 public void run () {
157- biometricPrompt .authenticate (promptInfo );
191+ try {
192+ Cipher cipher = getCipher ();
193+ SecretKey secretKey = getOrCreateSecretKey ();
194+ cipher .init (Cipher .ENCRYPT_MODE , secretKey );
195+ biometricPrompt .authenticate (promptInfo , new BiometricPrompt .CryptoObject (cipher ));
196+ } catch (KeyPermanentlyInvalidatedException ex ) {
197+ Log .e ("run#KeyPermanently" , ex .toString ());
198+ removeSecretKey ();
199+ if (isInit ) {
200+ biometricPrompt .authenticate (promptInfo );
201+ } else {
202+ p .resolve (makeAuthorizationResponse (false , FINGERPRINT_CHANGE ));
203+ }
204+ } catch (InvalidKeyException ex ) {
205+ Log .e ("run#InvalidKey" , ex .toString ());
206+ p .resolve (makeAuthorizationResponse (false , BiometricPrompt .ERROR_LOCKOUT ));
207+ }
158208 }
159209 }
160210 );
@@ -225,7 +275,7 @@ private BiometricPrompt.PromptInfo buildBiometricPrompt(ReadableMap options) {
225275 * @return string
226276 */
227277 private String convertErrorCode (int errorCode ) {
228- switch (errorCode ) {
278+ switch (errorCode ) {
229279 /**
230280 * No biometric features available on this device.
231281 */
@@ -311,6 +361,8 @@ private String convertErrorCode(int errorCode) {
311361 */
312362 case AUTHORIZATION_FAILED :
313363 return "AuthenticationFailed" ;
364+ case FINGERPRINT_CHANGE :
365+ return "FingerprintChange" ;
314366 default :
315367 //return "Unexpected error: " + String.valueOf(errorCode);
316368 return null ;
@@ -324,4 +376,54 @@ private String convertErrorCode(int errorCode) {
324376 public void release () {
325377 biometricPrompt = null ;
326378 }
379+
380+ @ RequiresApi (api = Build .VERSION_CODES .N )
381+ private SecretKey getOrCreateSecretKey () {
382+ try {
383+ KeyStore keyStore = KeyStore .getInstance ("AndroidKeyStore" );
384+ // Before the keystore can be accessed, it must be loaded.
385+ keyStore .load (null );
386+ SecretKey secretKey = ((SecretKey ) keyStore .getKey (KEY_NAME , null ));
387+ if (secretKey != null ) {
388+ return secretKey ;
389+ }
390+ KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec .Builder (
391+ KEY_NAME ,
392+ KeyProperties .PURPOSE_ENCRYPT | KeyProperties .PURPOSE_DECRYPT )
393+ .setBlockModes (KeyProperties .BLOCK_MODE_CBC )
394+ .setEncryptionPaddings (KeyProperties .ENCRYPTION_PADDING_PKCS7 )
395+ .setUserAuthenticationRequired (true )
396+ .setInvalidatedByBiometricEnrollment (true )
397+ .build ();
398+ KeyGenerator keyGenerator = KeyGenerator .getInstance (KeyProperties .KEY_ALGORITHM_AES , "AndroidKeyStore" );
399+ keyGenerator .init (keyGenParameterSpec );
400+ return keyGenerator .generateKey ();
401+ } catch (Exception e ) {
402+ Log .e ("generateKey" , e .toString ());
403+ }
404+ return null ;
405+ }
406+
407+ @ RequiresApi (api = Build .VERSION_CODES .M )
408+ private Cipher getCipher () {
409+ try {
410+ return Cipher .getInstance (KeyProperties .KEY_ALGORITHM_AES + "/"
411+ + KeyProperties .BLOCK_MODE_CBC + "/"
412+ + KeyProperties .ENCRYPTION_PADDING_PKCS7 );
413+ } catch (NoSuchPaddingException | NoSuchAlgorithmException e ) {
414+ Log .e ("getCipher" , e .toString ());
415+ }
416+ return null ;
417+ }
418+
419+ private void removeSecretKey () {
420+ try {
421+ KeyStore keyStore = KeyStore .getInstance ("AndroidKeyStore" );
422+ // Before the keystore can be accessed, it must be loaded.
423+ keyStore .load (null );
424+ keyStore .deleteEntry (KEY_NAME );
425+ } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e ) {
426+ Log .e ("removeSecretKey" , e .toString ());
427+ }
428+ }
327429}
0 commit comments