oblador / react-native-keychain

:key: Keychain Access for React Native
MIT License
3.19k stars 520 forks source link

Keychain on Android always reports Fingeprint Biometry Type #301

Open vascofg opened 4 years ago

vascofg commented 4 years ago

Testing out the new Android support on a Pixel 4 and the library always reports Fingerprint as the supported biometry type. You should use getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE): more info https://developer.android.com/reference/android/content/pm/PackageManager#FEATURE_FACE

I can try making a PR for this later this week / early next week.

StoyanD commented 4 years ago

having the same issue with pixel 4. It is misinterpreting the FEATURE_FACE as a fingerprint, and then crashing when i try to get generic password because fingerprint is not set up. Same issue with Galaxy A70. I think PackageManager.FEATURE_FACE completely wreaks this library on android

kuhnza commented 4 years ago

I'm also experiencing the same issue.

Without waiting for Face biometry to be implemented is there a way to check if a fingerprint has been setup? Ideally getSupportedBiometryType() would only return Fingerprint if it has been configured by the user.

kuhnza commented 4 years ago

Correction to my post above. It turns out that fingerprints are in fact enrolled and getSupportedBiometryType() returns Fingerprint as it should. However the preferred biometric on the device is set to Face recognition.

The library then allows the setGenericPassword to be called with { accessControl: ACCESS_CONTROL.BIOMETRY_CURRENT_SET }. A subsequent call to getGenericPassword then raises the permissions dialog only showing Face and even if granted the call fails.

Screenshot_20200331-085418_Settings Screenshot_20200331-085428_Settings Screen Shot 2020-03-31 at 9 14 19 am

vascofg commented 4 years ago

@kuhnza not sure this is the full story, as I was testing with a Pixel 4 which doesn't have a fingerprint scanner, only face recognition, and experienced the same.

kuhnza commented 4 years ago

Interesting. Diving into the code I think I can see the issue.

https://github.com/oblador/react-native-keychain/blob/9dd18b17e34c966c9b8856a312f828f2fd646d09/android/src/main/java/com/oblador/keychain/DeviceAvailability.java#L20

This code doesn't check if Fingerprint is available, it checks if any biometry is available on the device. This means it could be Fingerprint, Face, both or anything else Android chooses to add support for in future for that matter (see [https://developer.android.com/reference/android/hardware/biometrics/BiometricManager#canAuthenticate()](https://developer.android.com/reference/android/hardware/biometrics/BiometricManager#canAuthenticate())).

There's a couple of options as far as I can tell: 1) Restrict test to only return non-null response for Fingerprint 2) Add support for Face and return preferred from getSupportedBiometryType()

The first option is likely simpler to implement and fixes the immediate problem. It does rely on older APIs but something like this might get the job done:

FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
return fm.isHardwareDetected() && fm.hasEnrolledFingerprints();

The second option is far better from a user and device support standpoint. Obviously you'd eventually want to support both Face and Fingerprint across both platforms. Judging by the code it looks like it's a lot more involved but TBH I haven't studied it that closely.

@OleksandrKucherenko or @vonovak do either of you have a view on this? Supporting biometrics on Android in the wild is pretty much a non-starter until this is resolved.

kuhnza commented 4 years ago

After sleeping on it I realised the code for the first option doesn't completely resolve the issue since it doesn't tell you that the device only supports fingerprint. Back to the drawing board.

vonovak commented 4 years ago

hello, from a maintainer viewpoint, I prefer the right solution rather than a quick workaround. If you open a PR, please provide as much context as reasonably possible (and ideally add tests) so that it can be properly reviewed. The android code is indeed not straightforward and OleksandrKucherenko has the most up-to-date knowledge of it, so he'd be best reviewer.

OleksandrKucherenko commented 4 years ago

will be good to catch crash logs from the device... without logs its hard to repeat the crash, especially when you don't have a pixel4 device ;)

tudormarze commented 4 years ago

Hello everyone,

I got the same problem reported from a Pixel 4 device. From my understanding the Android SDK does not allow to know which specific authentication method was enabled (face/finger/iris), in Android R it only added an API for checking if it's BIOMETRIC_WEAK or BIOMETRIC_STRONG ( from here )

I was thinking there are a few happy cases where the device has hardware for only one type of biometric, but for those who have at least two there would probably be needed some 'Generic Biometric' value

Code for checking I was thinking of (Kotlin):

val hasFaceAuth = packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)
val hasFingerprintAuth = packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
val hasIrisAuth = packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)
val isEnabled = BiometricManager.from(this).canAuthenticate() == BIOMETRIC_SUCCESS

if (isEnabled && hasFingerprintAuth && !hasFaceAuth && !hasIrisAuth) {
    // has only fingerprint
} else if (isEnabled && !hasFingerprintAuth && hasFaceAuth && !hasIrisAuth) {
    // has only face
} else if (isEnabled && !hasFingerprintAuth && !hasFaceAuth && hasIrisAuth) {
    // has only iris
} else if (isEnabled) {
    // generic Biometric; can be any of them
}

What are your thoughts on this?

Thank you.

vascofg commented 4 years ago

@tudormarze I've actually used just hasSystemFeature(PackageManager.FEATURE_FACE) in the past with success, on a device with both face recognition and a fingerprint sensor (Galaxy S9). It returned true when I had enrolled face recognition and false when not.

tudormarze commented 4 years ago

@vascofg ok, I think that's a bit odd. The documentation for this method states it returns true if the devices supports the feature, else false. I don't have any device with face recognition on which I could test

stueynet commented 4 years ago

@oblador could we bump this up? If you have any thoughts on this issue we could try to fork and create a fix for it. At the moment there doesn't seem to be a clear way to handle this one. Users with Face Unlock simply can't use it at the moment.

Is this the fix? https://github.com/oblador/react-native-keychain/pull/342

reesretuta commented 4 years ago

bump. any update on this issue? #342 does not seem to be the fix

sdumjahn commented 3 years ago

bump. I have the same problem. When only enrolled Iris the call of getSupportedBiometryType() returns null. When adding a fingerprint the call results to Fingerprint. When I try to read the credentials it prompts Iris because thats the preferred biometrics.

sgal commented 3 years ago

I think there is a bit of misunderstanding and confusion with the current API. To explain the current behaviour, we first need to understand how biometrics are classified in Android. I'll add a link to Android CDD that explains the classification in greater details.

There are 3 classes of biometric sensors that are recognised by Android OS:

See Android CDD for details.

Addressing Preferred biometric setting mentioned above - it is a thing that is used by Android system features, like screen unlock and has no impact on the BiometricPrompt API. Only Class 2 and Class 3 are available for the use-cases of this library.

I hope this clarifies why some biometric types are not reported as available, despite being present on the device.

sgal commented 3 years ago

I created a PR with support of Class 3 biometric since it looks like it is the only class that can unlock the keystore - https://github.com/oblador/react-native-keychain/pull/449. There is an issue with the current implementation that BiometricManager.canAuthenticate() will return BIOMETRIC_SUCCESS if at least Class 2 (weak) biometric is present. Also, BiometricPrompt.authenticate is called without CryptoObject which allows the usage of Class 2 biometric, but as a consequence, does not unlock the Keystore, leaving an endless loop of "User not authenticated" errors.

If anyone could try and example app from my PR on the device with Face recognition and Fingerprint - I would be grateful. I only have OnePlus 6T, which has a Class 1 face recognition and Class 3 fingerprint, so I cannot really test the difference.

In my PR I updated androidx.biometric lib to the latest stable and it supports querying and enforcing only Class 3 biometry to make sure the Keystore is unlocked after the successful authentication.

FrozenPyrozen commented 3 years ago

Can we use only fingerprint and ignore face detection on Android?

sgal commented 3 years ago

@FrozenPyrozen It wouldn't cover all devices. Pixel 4 does not have a fingerprint sensor, but nonetheless has Class 3 face recognition that is capable of unlocking the Keychain. But make priority for the fingerprint if it is present on the device is for sure the way to go.

I verified this on Samsung Galaxy S10, which has Class 2 face recognition and Class 3 fingerprint. Only fingerprint is unlocking the Keystore, while face recog gives "User not authenticated" error even on successful verification.

fedeerbes commented 3 years ago

Hi @sgal, your PR #449 is stable enough if I just want to use biometric Fingerprint onAndroid. Android biometrics is not very stable at the moment in this library and it makes sense to me to only allow Fingerprint to my users.

sgal commented 3 years ago

@fedeerbes Yes, my PR is fixing the order of supported biometric types to offer fingerprint if it is present. That should sort out most of the instabilities in biometrics detection. I'm going to test my changes more thoroughly during the coming weekend and try to finalize the PR.

fedeerbes commented 3 years ago

Thanks @sgal. I currently have a Samsung Galaxy J6 and S8+. I'll do some tests with those devices in your PR and let you know

fedeerbes commented 3 years ago

After some testing I was not able to achieve only Fingerprint behavior on your PR @sgal. When I try to read the credentials it prompts other BiometryType selected because thats the preferred biometrics. I want to only let the user set and read credentials through Fingerprint.

sgal commented 3 years ago

@fedeerbes Thanks for testing, I'll try to debug it during the weekend.

hklee93 commented 3 years ago

Following this issue. Hoping #449 to be merged soon 🙏

sgal commented 3 years ago

@fedeerbes I tested and found the issue in the example, which is now fixed in my PR. I also verified that the fix works on Samsung Galaxy S20, see https://github.com/oblador/react-native-keychain/pull/449#issuecomment-817136644. Please try the latest commit in my PR #449 on your devices.

fedeerbes commented 3 years ago

Hi @sgal, thanks for taking care of this. I'll run some tests and let you know. I ended up using an AES storage and another library for biometrics, but if I can get #449 working that would be ideal as it will be a more secure option.

sgal commented 3 years ago

We did some more testing and here we are - 7.0.0 is released with strong biometric by default. So now majority of users will be prompted with Fingerprint to unlock the Keystore. Please try it out.

AlphaJuliettOmega commented 3 years ago

@sgal behavior seems to be different for older Android devices, Huawei P8 Lite: API level 21, Android 7 (there is no update for these devices)


2021-06-23 14:06:25.458 17459-17951/app.xxx W/CipherStorageBase: StrongBox security storage is not available.
    com.oblador.keychain.exceptions.KeyStoreAccessException: Strong box security keystore is not supported for old API24.
        at com.oblador.keychain.cipherStorage.CipherStorageBase.tryGenerateStrongBoxSecurityKey(CipherStorageBase.java:453)
        at com.oblador.keychain.cipherStorage.CipherStorageBase.generateKeyAndStoreUnderAlias(CipherStorageBase.java:408)
        at com.oblador.keychain.KeychainModule.internalWarmingBestCipher(KeychainModule.java:174)
        at com.oblador.keychain.KeychainModule.lambda$DYujhqpjRgfFQ_gyuwMwyxxqDlk(KeychainModule.java)
        at com.oblador.keychain.-$$Lambda$KeychainModule$DYujhqpjRgfFQ_gyuwMwyxxqDlk.run(lambda)
        at java.lang.Thread.run(Thread.java:776)
    I think the above warning is related to the subsequent:

        2021-06-23 14:06:36.039 17459-17953/app.xxx W/CipherStorageBase: User not authenticated
    android.security.keystore.UserNotAuthenticatedException: User not authenticated
        at android.security.KeyStore.getInvalidKeyException(KeyStore.java:718)
        at android.security.KeyStore.getInvalidKeyException(KeyStore.java:754)
        at android.security.keystore.KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit(KeyStoreCryptoOperationUtils.java:54)
        at android.security.keystore.KeyStoreCryptoOperationUtils.getExceptionForCipherInit(KeyStoreCryptoOperationUtils.java:89)
        at android.security.keystore.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:265)
        at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:109)
        at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2977)
        at javax.crypto.Cipher.tryCombinations(Cipher.java:2884)
        at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2789)
        at javax.crypto.Cipher.chooseProvider(Cipher.java:956)
        at javax.crypto.Cipher.init(Cipher.java:1199)
        at javax.crypto.Cipher.init(Cipher.java:1143)
        at com.oblador.keychain.cipherStorage.CipherStorageBase$Defaults.lambda$static$1(CipherStorageBase.java:519)
        at com.oblador.keychain.cipherStorage.-$$Lambda$CipherStorageBase$Defaults$DeW6NXOzsQTAPQNNW0rqTXPHW4c.initialize(lambda)
        at com.oblador.keychain.cipherStorage.CipherStorageBase.decryptBytes(CipherStorageBase.java:377)
        at com.oblador.keychain.cipherStorage.CipherStorageBase.decryptBytes(CipherStorageBase.java:332)
        at com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb.decrypt(CipherStorageKeystoreRsaEcb.java:127)
        at com.oblador.keychain.KeychainModule.decryptToResult(KeychainModule.java:669)
        at com.oblador.keychain.KeychainModule.decryptCredentials(KeychainModule.java:636)
        at com.oblador.keychain.KeychainModule.getGenericPassword(KeychainModule.java:296)
        at com.oblador.keychain.KeychainModule.getGenericPasswordForOptions(KeychainModule.java:357)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
        at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:151)
        at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
        at android.os.Handler.handleCallback(Handler.java:761)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:27)
        at android.os.Looper.loop(Looper.java:156)
        at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:226)
        at java.lang.Thread.run(Thread.java:776)
    The awkward result is a 'successful get' with an empty result, when using the fingerprint reader to unlock the upgraded-to-biometrics keychain:

    username edited out, password is just an empty string as quoted:
         LOG    Successful Get: {"password": "", "service": "", "storage": "KeystoreRSAECB", "username": "xxx"}
devpascoe commented 2 years ago

What are folks' recent experience with this? We're running react-native-keychain 8.0.0 on RN 0.67.3. iOS seems fine. Android:

Seems face is ignored altogether if android has fingerprint support.

sgal commented 2 years ago

@devpascoe Face method is not ignored. On most devices face recognition is not considered strong biometry. In order to get access to Android Keystore where secrets are located, user needs to complete Strong biometry challenge. On devices that have both face and fingerprint - the latter is always strong and the former is not.

The only device that has strong face biometry is Pixel 4. But it also does not have any other biometric sensors.

Hope this clarifies the behaviour you are facing.

devpascoe commented 2 years ago

Appreciate that info @sgal. Do you know if the library prioritises fingerprint over face even if there are no fingers registered? Basically i'd like to prioritise Face over Fingerprint, is that an option within the library currently?

sgal commented 2 years ago

@devpascoe The library prioritises fingerprint, yes. Face won't work because it is not able to decrypt the secrets, so the operation would fail with "User not authenticated" error.

arlovip commented 9 months ago

So Face is not supported on Android now? Could anyone confirm this? I set the Face and the fingerprint is turned off. But the Keychain.getSupportedBiometryType always returns null.

SuganyaEkambaram commented 8 months ago

Anyone confirm , so we cant authenticate with Face in Android using this plugin. if only the face is enrolled in the device

sgal commented 8 months ago

@SuganyaEkambaram Yes, face biometric is not classified as Strong, so it cannot be used to unlock the Keystore.

There is one exception - Pixel phones where it is not true. But this library does not have that exception, so face biometric is not supported here.