Skip to content

Commit 483d9d5

Browse files
author
kien.nguyen
committed
add cipher when biometricPrompt
1 parent e9daf61 commit 483d9d5

2 files changed

Lines changed: 113 additions & 9 deletions

File tree

android/src/main/java/com/bebnev/RNLocalAuthentication/RNLocalAuthenticationModule.java

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
11
package 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+
39
import androidx.annotation.NonNull;
10+
import androidx.annotation.RequiresApi;
411
import androidx.fragment.app.FragmentActivity;
512
import androidx.biometric.BiometricPrompt;
613
import androidx.biometric.BiometricManager;
14+
715
import static androidx.biometric.BiometricConstants.ERROR_NEGATIVE_BUTTON;
816
import 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;
929
import java.util.concurrent.Executor;
1030

1131
import com.facebook.react.bridge.ReadableMap;
@@ -18,11 +38,19 @@
1838
import com.facebook.react.bridge.WritableMap;
1939
import 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)
2249
public 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
}

src/LocalAuthentication/types/authentication.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,6 @@ export type AuthenticateOptionsAndroid = AuthenticateOptionsDefault & {
6666
* Negative button text should not be set if `fallbackToPinCodeAction` is set to true.
6767
*/
6868
cancelTitle?:string;
69+
70+
isInit?: boolean;
6971
};

0 commit comments

Comments
 (0)