react-native-keychain
react-native-keychain copied to clipboard
Keychain on Android always reports Fingeprint Biometry Type
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.
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
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.
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.
@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.
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()).
There's a couple of options as far as I can tell:
- Restrict test to only return non-null response for Fingerprint
- 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.
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.
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.
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 ;)
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.
@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.
@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
@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
bump. any update on this issue? #342 does not seem to be the fix
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.
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:
- Class 1 (aka convenience) - the weakest biometric type, can only be used for operations that do not require strong user authentication. An example is face recognition on OnePlus 6T+ devices. You can use it to unlock your phone, but not much more. Depending on implementation, Iris biometric can also be classified as Class 1 and not being available in the API. Important: This biometric type is not exposed to applications because of its low-reliability rate.
-
Class 2 (aka weak) - More precise and reliable than Class 1, but still allows a pretty big margin of spoofing (see link below for exact numbers). This biometric type is accessible in the API and can be used to authenticate the user (but not unlock the Keystore), by calling
BiometricPrompt.authenticate(promptInfo)
API docs. By default, a call toBiometricManager.canAuthenticate()
is resolving toBIOMETRIC_SUCCESS
if at least a Class 2 biometric sensor is available. An example of this type of sensor can be some implementations of Face recognition on some devices (with the exception of Pixel 4 which implemented a Class 3 face recognition biometric sensor). -
Class 3 (aka strong) - The most precise and spoofing-proof sensor (usually fingerprint). Same as Class 2, available for applications to use via API. Can be used to perform high-security per-operation authentications on KeyStore. For example, authenticating a decryption operation of a single value, locking the KeyStore after the operation. This sort of biometric can be invoked specifically, by calling
BiometricPrompt.authenticate(promptInfo, cryptoObject)
API docs which will fail if Class 3 biometric sensor is not available.
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.
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.
Can we use only fingerprint and ignore face detection on Android?
@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.
Hi @sgal, your PR #449 is stable enough if I just want to use biometric Fingerprint
on Android
. 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.
@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.
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
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
.
@fedeerbes Thanks for testing, I'll try to debug it during the weekend.
Following this issue. Hoping #449 to be merged soon 🙏
@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.
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.
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.
@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"}
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:
- fingerprint not configured + face is configured = no prompt for auth
- fingerprint is configured + face is configured = prompt for fingerprint auth
Seems face is ignored altogether if android has fingerprint support.
@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.