codenameone / CodenameOne

Cross-platform framework for building truly native mobile apps with Java or Kotlin. Write Once Run Anywhere support for iOS, Android, Desktop & Web.
https://www.codenameone.com/
Other
1.7k stars 403 forks source link

RFE : Biometric Authentication Module #3045

Open pkhashim opened 4 years ago

pkhashim commented 4 years ago

Hi, We have now upgraded our subscription to the enterprise level.

Can you kindly enhance the current fingerprint library with the below functionalities?

  1. Upgradation of the current fingerprint library to the latest available biometric module in both IOS & Android.

  2. Face & touch Id authentication.

  3. Credential management using the Keystore mechanism.

Thank you,

pkhashim commented 4 years ago

Hi Team,

Kindly update the status of the issue? When can I expect the requested changes in production?

Thanks, Hashim

shannah commented 4 years ago

There will be an update when there is something to update. This is in my queue, but haven't started work on it yet.

shannah commented 4 years ago

Some initial research

Upgradation of the current fingerprint library to the latest available biometric module in both IOS & Android.

Face & touch Id authentication.

Credential management using the Keystore mechanism.

Will need to create an new cn1lib for this. API should follow the iOS API Android doesn't have direct API for storing passwords.

Rough API:

/**
 * A class for storing content securely in the device keychain.
 */
public class KeyChain
   /**
    * Obtain device's default keychain.
    */
    public static KeyChain getDefaultKeychain();

    /**
     * Return true if the current platform supports the KeyChain api.
     */
    public static boolean isSupported();

    /**
     * Stores value securely in keychain.
     *  @param key A key to lookup the secure value.
     *  @param value A value to store securely in the keychain.
     */
    public void put(String key, String value);

    /**
     * Gets a secure value from the keychain.
     */
    public String get(String key);

    /**
     * Removes a value from the keychain.
     */
    public void remove(String key);

The iOS implementation would be a thin abstraction over the SecItemAdd function.

The android implementation would store an encrypted Map in storage, and it would use a key to decrypt/encrypt this map, per this article.

Some references:

pkhashim commented 4 years ago

Dear Steve,

Thank you for your response. We would like to have a detailed discussion on these matters over the phone. Kindly revert back to us at your convenient time.

Thanks & Regards

On Mon, Mar 23, 2020 at 8:10 PM Steve Hannah notifications@github.com wrote:

Some initial research

Upgradation of the current fingerprint library to the latest available biometric module in both IOS & Android.

Face & touch Id authentication.

  • iOS already supports.
  • Android API 28+ can be made to support using BiometricPrompt, but only on API 28+. See notes above.

Credential management using the Keystore mechanism.

Will need to create an new cn1lib for this. API should follow the iOS API Android doesn't have direct API for storing passwords.

Rough API:

/**

  • A class for storing content securely in the device keychain. */ public class KeyChain /**

    • Obtain device's default keychain. */ public static KeyChain getDefaultKeychain();

    /**

    • Return true if the current platform supports the KeyChain api. */ public static boolean isSupported();

    /**

    • Stores value securely in keychain.
    • @param key A key to lookup the secure value.
    • @param value A value to store securely in the keychain. */ public void put(String key, String value);

    /**

    • Gets a secure value from the keychain. */ public String get(String key);

    /**

    • Removes a value from the keychain. */ public void remove(String key);

The iOS implementation would be a thin abstraction over the SecItemAdd https://developer.apple.com/documentation/security/1401659-secitemadd?language=objc function.

The android implementation would store an encrypted Map in storage, and it would use a key to decrypt/encrypt this map, per this article https://medium.com/@ericfu/securely-storing-secrets-in-an-android-application-501f030ae5a3 .

Some references:

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/codenameone/CodenameOne/issues/3045#issuecomment-602699637, or unsubscribe https://github.com/notifications/unsubscribe-auth/AKZ5B5ANCSDE2TGLYRHR5STRI6CWZANCNFSM4LGIM5GA .

-- Hashim Peruvan Kuzhi, Mob: +971 55 75 163 75

Email: hashimpk.mec@gmail.com

shannah commented 4 years ago

@pkhashim Follow up in the support channel and we'll arrange a meeting there.

shannah commented 4 years ago

Based on phone conversation, I'm updating the requirements.

  1. Keychain API/cn1lib is unnecessary
  2. The Fingerprint scanner API needs to be improved to return a Key pair on success:
Fingerprint.scanFingerprint("Use your finger print to unlock AppName.", value -> {

    // value should be some sort of crypto object that contains a keypair.
    Log.p("Scan successfull!");
}, (sender, err, errorCode, errorMessage) -> {
    Log.p("Scan Failed!");
});

The success callback needs be provided with a crypto object similar to the BiometricPrompt.CryptoObject. At this point we only need a keypair that can be used to encrypt and decrypt data using bouncy castle.

On iOS there is no similar mechanism to access the key pair associated with an authentication, so, upon successful validation, the first time, we need to generate a key pair, then store it in the keychain with SecAddItem and the kSecAccessControlTouchIDCurrentSet flag to ensure that the key gets invalidated if the user adds/removes fingers.

On subsequent scans, it will return that key pair from the keychain.

Other references

. FingerprintScanner cn1lib . BouncyCastle cn1lib - This lib won't depend on bouncy castle, but you'll need to use this cn1lib to be able to make use of the key pair for encryption/decription.

shannah commented 4 years ago

I decided to opt for an implementation closer to my original design. Rather than expose the cryptographic object keys, I decided to simply provide APIs to store and retreive passwords (or arbitrary strings) securely in the system keychain, protected by biometric authentication.

I made this decision because exposing the cryptographic object would result in too much surface area in the API. There are too many settings for creating the various kinds of keys that would all have to have APIs created for them. Additionally, on iOS, they do things much differently.

I've added documentation about this in the readme for the cn1lib here:

https://github.com/codenameone/FingerprintScanner#protecting-passwords-with-fingerprints

I've just pushed out a first phase of this upgrade which includes full support for this password API on both iOS and Android. You'll be able to update through Codename One settings soon. In the mean time you can install the lib manually from https://github.com/codenameone/FingerprintScanner/blob/master/bin/FingerprintScanner.cn1lib[here].

No Biometric Prompt Yet

This first phase uses FingerprintScanner on Android rather than Biometric prompt. This is because I could achieve all of the required functionality using FingerprintScanner, and be able to support devices all the way back to API 23. BiometricPrompt requires API 28+. In the next phase (sometime next week), I'll add support for BiometricPrompt so that devices that support it (API 28+) will use that instead.

On iOS, no changes will be necessary in that next iteration.

pkhashim commented 4 years ago

We have tested the initial version that you had uploaded and identified some issues in both iOS and Android implementation.

In the Android implementation, there is no callback available in case of a successful delete password call.

Code: Fingerprint.deletePassword("Getting secure item", key).onResult((res, err)->.

In the case of iOS implementation, it is not asking for the fingerprint while registering/adding the password.

Also, there is no fingerprint prompt is available for both iOS and Android implementation in the delete password method call.

Kindly revert back to us at your convenient time.

shannah commented 4 years ago

In the Android implementation, there is no callback available in case of a successful delete password call.

This is fixed. 4e93d94. Update lib from here. It will be updated in Codename One settings soon.

In the case of iOS implementation, it is not asking for the fingerprint while registering/adding the password.

This seems to be the way that iOS works for adding items to the keychain. This is a thin layer over the SecItemAdd function. There are likely scenarios in which it will prompt you for a fingerprint on SecItemAdd, and from my reading this is somewhat inconsistent by iOS version. Old versions will always prompt, but later versions seem to be smarter about whether it is required.

I could add an explicit prompt here but it wouldn't really add any security because the user could just add their fingerprint and then return to the app to insert the password in the case of a user who doesn't already have a fingerprint in the system. This is likely why Apple doesn't show the prompt here.

Also, there is no fingerprint prompt is available for both iOS and Android implementation in the delete password method call.

This is expected behaviour. If you want the user to be prompted, you can first "get" the password, and then delete it. The user will be prompted during the "get" action.

pkhashim commented 4 years ago

Thanks for fixing the callback issue in the android implementation regarding deletepassword method.

Can you kindly enable the explicit prompt in the case of iOS biometric registration/adding?

Also, we are expecting one method to identify the type of biometric features that are enabled in the device, whether it is Face ID, Finger ID or both?

shannah commented 4 years ago

Can you kindly enable the explicit prompt in the case of iOS biometric registration/adding?

I'll add an optional display property for this behaviour as it doesn't add any additonal security and it is likely to conflict with some future (or past) iOS version (conflict as in, user may be prompted for fingerprint twice in some edge cases that are hard to detect).

Also, we are expecting one method to identify the type of biometric features that are enabled in the device, whether it is Face ID, Finger ID or both?

OK. I'll add this.

shannah commented 4 years ago

Can you kindly enable the explicit prompt in the case of iOS biometric registration/adding?

This is done. You need to set the "ios.Fingerprint.addPassword.prompt" display property to "true".

Also, we are expecting one method to identify the type of biometric features that are enabled in the device, whether it is Face ID, Finger ID or both?

I've added isTouchIDAvailable() and isFaceIDAvailable() static methods now.

You can download updated lib here. It will be in Codename One settings soon.

pkhashim commented 4 years ago

Good day Steve.

Thanks for the prompt response and for addressing the concerns.

If multiple biometric features are enabled in one device, how can we specifically use/invoke those features? For example, in Samsung S9+, the available biometric features are iris scanner, fingerprint scanner, and face recognition. How the user can register for all these features? And can we specifically register for one type of biometric feature? And if all of these features are registered, will the API give any prompt to authenticate with a selected feature? Generally, how we will handle addPassword, getPassword and deletePassword methods?

shannah commented 4 years ago

If multiple biometric features are enabled in one device, how can we specifically use/invoke those features?

As far as I can tell, iOS just chooses whatever the device has, and it's either or. If the device has Face iD (e.g. iPhoneX), it won't have touch ID. If it has touch ID, it won't have face ID.

Similar story on Android. You can specify "WEAK" or "STRONG", but I don't suggest exposing this API, and to just always use STRONG, since it says:

Authenticators#BIOMETRIC_WEAK for non-crypto authentication, or

Which we don't want. So it will just choose what the user has set up as long as qualifies as BIOMETERIC_STRONG - which could be any of fingerprint, iris, or face recognition.

Android does allow you to "allow" users to use a password instead - but our cn1lib doesn't do any such thing. This library is biometric/fingerprint or bust right now.

pkhashim commented 4 years ago

Good day Steve.

Can you kindly provide a method to identify which biometric feature is a "STRONG"/recommended feature? So that we can design our front end accordingly and we will able to provide the appropriate icons in the app in case of multiple features are available.

pkhashim commented 4 years ago

Dear Team,

Good day.

We are awaiting the updates regarding the integration of BiometricPrompt. Can we start using the features?

Thanks

shannah commented 4 years ago

Sorry for the delay on this. I don't currently have any test devices on Android 9. I just did some google searches to see about upgrading my Galaxy S8 to Android 9 and it looks like it may be possible. I'm starting the update process now (it makes me do one update at a time... I'll see if 9.0 is at the end of that tunnel).

shannah commented 4 years ago

19 (!!) system updates later, my S8 is running Android 9. I'll start working on this tomorrow.

pkhashim commented 4 years ago

Thank you for the updates on this. We are about to launch our products in a couple of days. So kindly speed up the process and revert back so that we will able to give all the features to our customers without any troubles. Your cooperation is very much appreciated.

shannah commented 4 years ago

Still working on it. It is proving to be more difficult than expected as it follows a different workflow than Fingerprint manager, and has some new dependencies (e.g. AndroidX, FragmentActivity, etc..). This will take a few more days.

shannah commented 4 years ago

I managed to get an initial implementation working with the library. Still need to update some tools on the build server so that it will compile the SDK 29 stuff. Should have something to you within the next few days.

shannah commented 4 years ago

Note to self: I posted this question on Stack Overflow to help with this issue: https://stackoverflow.com/questions/62430155/biometricmanager-on-android-9

shannah commented 4 years ago

@pkhashim It seems that proper Android support for facial recognition does not exist until Android 11. Technically it is supported on Android 9 and Android 10, but there are fairly major caveat that in Android 10 and below there is no way to know whether the device supports facial recognition.

Android doesn't add the ability to check this until Android 11.

Therefore we can't provide a reliable implementation of FingerprintScanner.isFaceIDAvailable() when running on Android. The best I can do is have it return false, and have an another method "mightFaceIDBeAvailable()" that returns true if face ID might be available.

Android 11 is still in beta, so I don't imagine that you were planning on targeting it with your release in a couple of days. Additionally I don't have any test devices set up for Android 11 yet, so I can't really target it.

Therefore, I plan to implement it as follows:

  1. On android, isFaceIDAvailable() will always return false.
  2. On Android, I'll add mightFaceIDBeAvailable() which will return true iff touch ID is available, and android version is 9 or higher.

If I learn any tricks on how to find out this information, I'll adjust accordingly.

shannah commented 4 years ago

Actually, it's slightly worse than this. BiometricPrompt doesn't actually support face recognition in Android 9 - it only supports fingerprint scanning.

So we're looking at an Android 10 only feature for now. 9 and earlier will continue to use FingerprintManager. The problem here is that my newest Android test device currently doesn't support Android 10. I'm going to have a get a new test device in order to add face recognition.

shannah commented 4 years ago

I have ordered an android 10 test device, that should arrive tomorrow night. I should be able to use that to push out an update with facial recognition on Android by Tuesday.

shannah commented 4 years ago

The Android module will now use the BiometricPrompt API on Android 10 and up. You need to use build tools 29 or higher for this to work. E.g.

android.buildToolsVersion=29.0.3
android.targetSdkVersion=29.0.3

Note: The Android 10 test device that I acquired does have some Face ID support the Biometrics API still seems to only provide access to fingerprint scanning. I suspect this is related to my device rather than the lib as I stepped through the code with the debugger to ensure that it was following the path that uses BiometricPrompt and it is.

shannah commented 4 years ago

Oh. One other thing. The latest will be available in Codename One settings in next update. You can get the latest now at https://github.com/codenameone/FingerprintScanner/tree/master/bin

Install instructions at:

https://github.com/codenameone/FingerprintScanner#installation

pkhashim commented 4 years ago

Dear Steve,

I am using OnePlus 6 T, Android 10(Version 10.3.4), on my device I have both touch id and face id authentications enabled. Please see below the issues that I am facing.

  1. I have updated the latest changes that you provided regarding the face id prompt but it is not working as expected. The method 'Fingerprint.mightFaceIDBeAvailable()' is returning 'false', if only face id is enabled in the device. If I enable both the features together or enable only the touch id, this method returns true. The other method 'Fingerprint.isFaceIDAvailable()' is always returning 'false' only.

  2. If I enable both the features together in my device, the biometric prompt showing both the symbols(face id and touch id images). Kindly refer to attachment. (Attachment1.jpg).

    1. One of my colleagues is using Samsung S20, Android 10, there the device biometric prompt is overlapping with the biometric prompt that you have designed. Kindly refer to attachment. (Attachment2.jpg).

    2. On iOS, the method 'Fingerprint.addPassword()' is not consistent, sometimes the biometric prompt is not appearing.

Please don't hesitate to contact us if you need further details about the issue that I mentioned.

Thanks & Regards,

Attachment2 Attachment1

shannah commented 4 years ago

I have updated the latest changes that you provided regarding the face id prompt but it is not working as expected. The method 'Fingerprint.mightFaceIDBeAvailable()' is returning 'false', if only face id is enabled in the device. If I enable both the features together or enable only the touch id, this method returns true. The other method 'Fingerprint.isFaceIDAvailable()' is always returning 'false' only.

This is consistent with my findings on my Android 10 device. My tentative conclusion was that, although this device does have Face ID authentication support for logging into the phone, it somehow is not available to the BiometricPrompt API. Can you find any other 3rd-party apps that work with Face ID on that phone?

If I enable both the features together in my device, the biometric prompt showing both the symbols(face id and touch id images). Kindly refer to attachment. (Attachment1.jpg).

I'll remove the CN1 "fingerprint/faceid" graphic on devices that use the BiometricPrompt API

On iOS, the method 'Fingerprint.addPassword()' is not consistent, sometimes the biometric prompt is not appearing.

This is the built-in behaviour of iOS. It won't reprompt you if it deems that you have provided authentication recently enough. This can't and shouldn't be fixed. Any attempt to "fix" it will result in double prompts from time to time.

pkhashim commented 4 years ago

Hi Steve,

  1. Regarding the face id prompt in the Android 10 devices, I think your conclusion is right, none of the third-party apps in my device is not showing the face id feature. But I have to check it in other Android 10 devices as well.

  2. Is there any update on the issue numbers 2 & 3 of the previous mail?

  3. If we enable multiple touch ids in the device and then enable touch id login in our app, then if we remove one touch id signature from the device, the app still allowing to login with remaining touch ids. Is this the expected behavior? Can you please disable the biometric authentication if the biometric signature is altered, that is biometric signature is got added or deleted.

  4. Kindly provide a separate indicator whenever the biometric signature is altered, such as added a new signature or deleted existing signature, instead of just throwing error, so that we will able to prompt users accordingly.

Thanks, Hashim

shannah commented 4 years ago

Is there any update on the issue numbers 2 & 3 of the previous mail?

I have just updated the lib so that on API 29+ it will only show the native prompt and not the CN1 prompt. On 28- it still shows the CN1 prompt (not native).

If we enable multiple touch ids in the device and then enable touch id login in our app, then if we remove one touch id signature from the device, the app still allowing to login with remaining touch ids. Is this the expected behavior? Can you please disable the biometric authentication if the biometric signature is altered, that is biometric signature is got added or deleted.

All this logic is embedded into the native FingerprintScanner/BiometricPrompt code. Unless there is a config option for this (and I haven't found one), this is just how it works. It makes sense that removing a fingerprint shouldn't invalidate the key, but adding a fingerprint should.

Kindly provide a separate indicator whenever the biometric signature is altered, such as added a new signature or deleted existing signature, instead of just throwing error, so that we will able to prompt users accordingly.

This should be possible. I'll do some experimentation.

shannah commented 4 years ago

I've added an Exception, KeyRevokedException that you can check for when getting a password fails. It currently only works on Android. I been working on getting it working on iOS also, but have so far not been able to get it working without negative side-effects.

pkhashim commented 4 years ago

As part of the issues reported earlier, I have 2 more to highlight.

  1. On android buildx device S Note 7, the method 'Fingerprint.isTouchIDAvailable()' returns false only; note that the phone unlock method here is touch id.
  2. We have disabled the touch id authentication on Samsung S 20 after registering the touch id in our app, the method 'Fingerprint.isTouchIDAvailable()' still gives true only. Kindly advise.
shannah commented 4 years ago

This is difficult on Android. Android doesn't provide a way to know if the device has a specific type of auth. Things will improve a bit with API 30, but they don't seem to want to give you this information. They just tell us if authentication is supported - period. Could be fingerprint. Could be face ID. Could be iris. It's a bit of a guessing game.

pkhashim commented 4 years ago

Is there any update on the issue numbers 2 & 3 of the previous mail?

I have just updated the lib so that on API 29+ it will only show the native prompt and not the CN1 prompt. On 28- it still shows the CN1 prompt (not native).

I have updated the lib and now on my phone(One Plus 6 T, Android 10), both native and CN1 prompts are not coming. Earlier in my device, only the CN1 prompt was coming. Kindly revert.

pkhashim commented 4 years ago

As part of the issues reported earlier, I have 2 more to highlight.

  1. On android buildx device S Note 7, the method 'Fingerprint.isTouchIDAvailable()' returns false only; note that the phone unlock method here is touch id.
  2. We have disabled the touch id authentication on Samsung S 20 after registering the touch id in our app, the method 'Fingerprint.isTouchIDAvailable()' still gives true only. Kindly advise.

Is there any updates on these two issues reported?

pkhashim commented 4 years ago

I've added an Exception, KeyRevokedException that you can check for when getting a password fails. It currently only works on Android. I been working on getting it working on iOS also, but have so far not been able to get it working without negative side-effects.

The newly added exception is throwing initially only, next when I try to access API, it is throwing a different exception only. Kindly see the details below.

If I add new touch id and try to access the API, I will get the below exceptions:

"Failed to initialize cipher. Secret key must have been revoked"

And if I try one more time to access the API immediately after the first attempt, I am getting below exception.

"java.lang.RuntimeException: No key found".

Kindly throw the KeyRevokedException always if any new touch id is added.

shannah commented 4 years ago

I have updated the lib and now on my phone(One Plus 6 T, Android 10), both native and CN1 prompts are not coming. Earlier in my device, only the CN1 prompt was coming. Kindly revert.

You can still access the previous version of the cn1lib here:

https://github.com/codenameone/FingerprintScanner/blob/cc54f5067dbbb27bb6f5040af2c546f3c3160adc/bin/FingerprintScanner.cn1lib

As part of the issues reported earlier, I have 2 more to highlight.

On android buildx device S Note 7, the method 'Fingerprint.isTouchIDAvailable()' returns false only; note that the phone unlock method here is touch id. We have disabled the touch id authentication on Samsung S 20 after registering the touch id in our app, the method 'Fingerprint.isTouchIDAvailable()' still gives true only. Kindly advise.

What versions of Android are these running? The "touch ID available" is piped pretty much straight from the native API. On 28 and below it users the FingerprintScanner API. 29+ uses biometric prompt API.

The newly added exception is throwing initially only, next when I try to access API, it is throwing a different exception only. Kindly see the details below.

I think I should probably remove this feature since it can't be properly implemented cross platform. Getting it to work correctly in all situations on Android alone is challenging. On iOS it is even more difficult. It's unclear what value this adds.

pkhashim commented 4 years ago

I have updated the lib and now on my phone(One Plus 6 T, Android 10), both native and CN1 prompts are not coming. Earlier in my device, only the CN1 prompt was coming. Kindly revert.

You can still access the previous version of the cn1lib here:

https://github.com/codenameone/FingerprintScanner/blob/cc54f5067dbbb27bb6f5040af2c546f3c3160adc/bin/FingerprintScanner.cn1lib

Here the issue is that if you update to the latest version, then in OnePlus 6 T (Android 10), none of the prompts are coming and if we revert back to the previous version, both the prompts are appearing in Samsung S 20(Android 10). Kindly fix and provide a stable version.

As part of the issues reported earlier, I have 2 more to highlight. On android buildx device S Note 7, the method 'Fingerprint.isTouchIDAvailable()' returns false only; note that the phone unlock method here is touch id. We have disabled the touch id authentication on Samsung S 20 after registering the touch id in our app, the method 'Fingerprint.isTouchIDAvailable()' still gives true only. Kindly advise.

What versions of Android are these running? The "touch ID available" is piped pretty much straight from the native API. On 28 and below it users the FingerprintScanner API. 29+ uses biometric prompt API.

Here 2 issues are there.

Issue 1: Device : Samsung Note 4(Android version 6.0.1), the method 'Fingerprint.isTouchIDAvailable()' returns false only; note that the phone unlock method is touch id only.

Issue 2: Device : Samsung S 20 (Android version 10), we can disable the touch id signatures in the device(no need to delete the touch id signatures, just use the toggle button to enable/disable the touch ids after registering it in the device). We have disabled the touch id signatures in the device after registering the same in our app, the method 'Fingerprint.isTouchIDAvailable()' still gives true only. Is this the expected work flow? If the touch ids are disabled in the device, we cannot use the touch id to unlock the device, but the API is still allowing to use the touch ids in our app. Kindly advise.

The newly added exception is throwing initially only, next when I try to access API, it is throwing a different exception only. Kindly see the details below.

I think I should probably remove this feature since it can't be properly implemented cross platform. Getting it to work correctly in all situations on Android alone is challenging. On iOS it is even more difficult. It's unclear what value this adds.

In case if the bio-metric key is revoked, then users have to delete the current bio-metric signatures from the app and re-register it again. At the time of deleting the bio-metric signatures, we are asking for the authentication by calling method 'Fingerprint.getPassword'. If you can throw the 'KeyRevokedException', then app will able to delete signatures internally. kindly do not hesitate to contact us if you need further details.

shannah commented 4 years ago

I have made some changes that should address these issues. However, I haven't been able to test them yet due to some network issues.

This should address the prompt issues, and perhaps some of the detection of face ID and fingerprint support. Part of the problem is that Android seems to be moving away from giving you the direct ability to know what form of biometric authentication is available. I'm using the deprecated FingerprintManager API now on API 29 to check for fingerprint support. This feels dirty, but it really seems to be the only way to find out if fingerprint is supported, and there are fingerprints enrolled.

On API 29 and up, here is what we can detect:

  1. If the device supports Face ID (using PackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
  2. If Fingerprint Authentication is active, and the user has fingerprints enrolled. (Using deprecated FingerprintManager API)
  3. If the device supports Face ID (using PackageManager.hasSystemFeature(PackageManager.FEATURE_FACE))
  4. If some form of biometric authentication can be used to authenticate the user (this may be face, iris, or touch), using the BiometricManager API.

So, when we ask isTouchIDAvailable() - we should be able answer this question correctly (using the deprecated FingerprintManager API).

But when we ask isFaceIDAvailable(), we can't know for sure. Right now I'm returning "true" to this if the device has Face ID support, and they have some form of biometric authentication available. But that may be a false positive.

I haven't addressed the KeyRevokedException. This will be difficult to do correctly. I'm still not sure I understand why knowing that a previous key existed and was revoked is helpful. If you try to get the password, and it returns an error, it means that either the password was never added, or the key was revoked since the password was added. In either case, you need to insert the password again. If you want to distinguish between the password never having been entered, and the password being revoked, you could simply store a flag in Preferences the first time the password is set, to know that the password has been set. Ultimately, in order to implement such a KeyRevokedException cross-platform, I would need to do something similar to that behind the scenes.

https://github.com/codenameone/FingerprintScanner/blob/master/bin/FingerprintScanner.cn1lib

pkhashim commented 4 years ago

I have made some changes that should address these issues. However, I haven't been able to test them yet due to some network issues.

I have updated to the latest available and still, on my device(OnePlus 6 T, Android 10) none of the prompts are coming. Kindly advise.

This should address the prompt issues, and perhaps some of the detection of face ID and fingerprint support. Part of the problem is that Android seems to be moving away from giving you the direct ability to know what form of biometric authentication is available. I'm using the deprecated FingerprintManager API now on API 29 to check for fingerprint support. This feels dirty, but it really seems to be the only way to find out if fingerprint is supported, and there are fingerprints enrolled.

On API 29 and up, here is what we can detect:

  1. If the device supports Face ID (using PackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
  2. If Fingerprint Authentication is active, and the user has fingerprints enrolled. (Using deprecated FingerprintManager API)
  3. If the device supports Face ID (using PackageManager.hasSystemFeature(PackageManager.FEATURE_FACE))
  4. If some form of biometric authentication can be used to authenticate the user (this may be face, iris, or touch), using the BiometricManager API.

So, when we ask isTouchIDAvailable() - we should be able answer this question correctly (using the deprecated FingerprintManager API).

But when we ask isFaceIDAvailable(), we can't know for sure. Right now I'm returning "true" to this if the device has Face ID support, and they have some form of biometric authentication available. But that may be a false positive.

I haven't addressed the KeyRevokedException. This will be difficult to do correctly. I'm still not sure I understand why knowing that a previous key existed and was revoked is helpful. If you try to get the password, and it returns an error, it means that either the password was never added, or the key was revoked since the password was added. In either case, you need to insert the password again. If you want to distinguish between the password never having been entered, and the password being revoked, you could simply store a flag in Preferences the first time the password is set, to know that the password has been set. Ultimately, in order to implement such a KeyRevokedException cross-platform, I would need to do something similar to that behind the scenes.

https://github.com/codenameone/FingerprintScanner/blob/master/bin/FingerprintScanner.cn1lib

Here I will explain how our app is working:

  1. At the time of the biometric registration, we will call the method 'Fingerprint.addPassword' to store the login details.
  2. Next time when users try to log in, we will try to get the login the details using the method 'Fingerprint.getPassword' and login to the app.
  3. Users can unsubscribe from biometric authentication by disabling it. At the time of biometric disabling, we will call the method 'Fingerprint.getPassword' to identify the user and then call the method 'Fingerprint.deletePassword' to delete the login details from the key store.

In case if the Biometric Keys are revoked because of the addition of a new signature(s), we should able to tell the user that their biometric authentication has been revoked and they should re-register it. After your update, now we are getting the 'KeyRevokedException' at this point and we are able to give the proper message to the users. Then users will go to disable the biometric authentication(as explained in step 3) and then only they will able to register with new signatures. At that point, we are not getting the 'KeyRevokedException', though we are calling the method 'Fingerprint.getPassword'. If we get the correct exception at that point, instead of throwing error to the customers, we will able to disable the biometric authentication internally. But unfortunately, right now we are throwing errors to the users saying that because of 'some' error, we are unable to identify you and so that we will not allow them to disable the biometric authentication and we went to a deadlock(User is unable to login as well as they are unable to disable the existing biometric authentication). Biometric authentication can be failed for any reason like, no signatures are added, new signatures are added after registering it in the app, an illegitimate user is trying to access. So it is required to distinguish why biometric authentication is failed.

shannah commented 4 years ago

I have updated to the latest available and still, on my device(OnePlus 6 T, Android 10) none of the prompts are coming. Kindly advise.

I cannot reproduce on any of my devices. I don't know what to suggest. Double check that you have updated to the latest version https://github.com/codenameone/FingerprintScanner/blob/master/bin/FingerprintScanner.cn1lib and make sure you refresh cn1libs after changing it.

In case if the Biometric Keys are revoked because of the addition of a new signature(s), we should able to tell the user that their biometric authentication has been revoked and they should re-register it. After your update, now we are getting the 'KeyRevokedException' at this point and we are able to give the proper message to the users. Then users will go to disable the biometric authentication(as explained in step 3) and then only they will able to register with new signatures. At that point, we are not getting the 'KeyRevokedException', though we are calling the method 'Fingerprint.getPassword'. If we get the correct exception at that point, instead of throwing error to the customers, we will able to disable the biometric authentication internally. But unfortunately, right now we are throwing errors to the users saying that because of 'some' error, we are unable to identify you and so that we will not allow them to disable the biometric authentication and we went to a deadlock(User is unable to login as well as they are unable to disable the existing biometric authentication). Biometric authentication can be failed for any reason like, no signatures are added, new signatures are added after registering it in the app, an illegitimate user is trying to access. So it is required to distinguish why biometric authentication is failed.

If the user has added a signature (which you can detect and record), and has not deleted their signature through the app (which you can also detect and record), then if the user tries to get their signature, and you receive any exception, then really the only possibility is that their key has been revoked. I suppose the only other option is that they have disabled fingerprint authentication in their settings - but you can easily address that if there is an error adding a signature.

So I still don't see what information the KeyRevokedException adds, that you can't get other ways.

pkhashim commented 4 years ago

I have updated to the latest available and still, on my device(OnePlus 6 T, Android 10) none of the prompts are coming. Kindly advise.

I cannot reproduce on any of my devices. I don't know what to suggest. Double check that you have updated to the latest version https://github.com/codenameone/FingerprintScanner/blob/master/bin/FingerprintScanner.cn1lib and make sure you refresh cn1libs after changing it.

I have downloaded the cn1lib file(file size: 19573 bytes) and still, the prompts are not appearing in my device(Device: OnePlus 6 T, Android: 10). Kindly advise.

shannah commented 4 years ago

Can you try this sample app build, and confirm that you have the issue on this app with your OnePlus 6T on Android 10. https://cloud.codenameone.com/build/downloadResult?i=34ac535e-021e-4ea8-a055-703009ea7462&b=49af1fc7-418c-4867-8367-09fb9f4d98b3&n=FingerprintScannerSample-debug.apk

pkhashim commented 4 years ago

Can you try this sample app build, and confirm that you have the issue on this app with your OnePlus 6T on Android 10. https://cloud.codenameone.com/build/downloadResult?i=34ac535e-021e-4ea8-a055-703009ea7462&b=49af1fc7-418c-4867-8367-09fb9f4d98b3&n=FingerprintScannerSample-debug.apk

I am not getting any prompts on this sample app as well. Kindly see the attached screenshot. SampleApp

shannah commented 4 years ago

That screenshot shows a little teal fingerprint scan icon in the bottom of the screen. That appears to be the prompt that your phone provides.

Biometric prompt uses a native ui for this so we don't control it. Different phones apparently provide different user interfaces for this

shannah commented 4 years ago

This forum thread describes this issue with the Oneplus 6T exactly. Doesn't necessarily offer solutions, but it is someone expressing dissatisfaction with the OnePlus 6T biometric prompt UI.

https://forums.oneplus.com/threads/problems-about-oneplus-6t-fingerprint-api.944959/

pkhashim commented 4 years ago

This forum thread describes this issue with the Oneplus 6T exactly. Doesn't necessarily offer solutions, but it is someone expressing dissatisfaction with the OnePlus 6T biometric prompt UI.

https://forums.oneplus.com/threads/problems-about-oneplus-6t-fingerprint-api.944959/

I was getting the prompts earlier, now after the update, I do not get it anymore. Kindly check the screenshot. OnePlus6T

shannah commented 4 years ago

Here's the thing.

Android provides 2 APIs for fingerprint scanning.

  1. FingerprintScanner - Available since 6.0. Deprecated in 9.0. Supports fingerprint scanning only (no face ID). Doesn't provide any native UI - we need to provide the prompt ourselves.
  2. BiometricPrompt - Available since 9.0 - though doesn't support FaceID until 10.0. Provides native UI for the prompt.

On 9.0 and lower, I'm using Fingerprint scanner and providing our own UI prompt. On 10.0 and higher I'm using BiometricPrompt and NOT providing our own UI prompt.

That previous version that you shared the screenshot for had a bug that displayed both prompts on android 10. On the OnePlus 6T this looks OK because their native prompt is minimal. But you complained of seeing both prompts on other android 10 devices - and upon review I found the bug that was causing it to display both prompts.

This is a OnePlus-specific issue. It doesn't seem practical to try to guess whether the device's native biometric prompt is crappy, once we opt in to use the BiometricPrompt API. We could try to sniff for the OnePlus and display our own dialog anyway, but that is tricky because they may fix this bug in some version - and it's not clear exactly which versions of their phones have this issue. E.g. from the forum thread:

Can confirm this is still an issue. Also occurs on OnePlus 7 Pro. OnePlus 5T works. It seems to occur due to the inscreen fingerprint sensor. On the Galaxy S10 it does work as well.

The best solution for you here is probably to use the variant of getPassword() and addPassword() with the extra boolean parameter at the end to show android prompt. If true, this will display our own android prompt. On most Android 10 devices this defaults to false, and should so, because passing true here on most Android 10 devices will cause both prompts to appear, and they'll likely overlap.

/**
     * Gets a password from the keychain with option to hide android dialog.  
     * 
     * <p>This will prompt the user to authenticate using biometrics (fingerprint or face recognition).</p>
     * 
     * <p>Note:  If the user adds any new fingerprints or biometric validations to the device, all passwords
     * stored with the old set of fingerprints (etc..) will be invalidated.
     * @param reason Message to display in fingerprint scanning dialog that appears.
     * @param account The account name whose password we wish to obtain.
     * @param showDialogOnAndroid Set false to not show a dialog.  User would still need to use biometrics to authenticate
     * so if you set this to false, you should provide your own dialog on Android.
     * @return AsyncResource Use onResult() or ready() callback to obtain the password, or error condition.  If the password exists in 
     * the keychain, the result will be the password as a string.  If the password doesn't exist, then it will return an error.
     */
    public static AsyncResource<String> getPassword(String reason, String account, boolean showDialogOnAndroid)

/**
     * Adds a password to the keychain.  Some platforms may prompt the user to authenticate
     * to perform this function.  iOS (at least in iOS 13) will not authenticate the user for
     * this action.  It will only authenticate when the user wants to retrieve the password.
     * This is likely because adding authentication at this point wouldn't add any security
     * since the user could just add their fingerprint in "Settings", return to the app
     * and authenticate.  This same loop-hole doesn't exist for getting passwords because
     * passwords are automatically invalidated when adding additional fingerprints to the device.
     * 
     * <p>Enabling authentication for adding passwords on iOS can be achieved by setting 
     * the "ios.Fingerprint.addPassword.prompt" display property to "true"</p>
     * 
     * @param reason A message to display in the fingerprint authentication dialog.
     * @param account The account that the password is for.
     * @param password The password which you wish to store in the keychain.
     * @param showDialogOnAndroid Set false to not show a dialog.  User would still need to use biometrics to authenticate
     * so if you set this to false, you should provide your own dialog on Android.
     * @return AsyncResource that will return success/fail.
     */
    public static AsyncResource<Boolean> addPassword(String reason, String account, String password, boolean showDialogOnAndroid);
pkhashim commented 4 years ago

Here's the thing.

Android provides 2 APIs for fingerprint scanning.

  1. FingerprintScanner - Available since 6.0. Deprecated in 9.0. Supports fingerprint scanning only (no face ID). Doesn't provide any native UI - we need to provide the prompt ourselves.
  2. BiometricPrompt - Available since 9.0 - though doesn't support FaceID until 10.0. Provides native UI for the prompt.

On 9.0 and lower, I'm using Fingerprint scanner and providing our own UI prompt. On 10.0 and higher I'm using BiometricPrompt and NOT providing our own UI prompt.

That previous version that you shared the screenshot for had a bug that displayed both prompts on android 10. On the OnePlus 6T this looks OK because their native prompt is minimal. But you complained of seeing both prompts on other android 10 devices - and upon review I found the bug that was causing it to display both prompts.

This is a OnePlus-specific issue. It doesn't seem practical to try to guess whether the device's native biometric prompt is crappy, once we opt in to use the BiometricPrompt API. We could try to sniff for the OnePlus and display our own dialog anyway, but that is tricky because they may fix this bug in some version - and it's not clear exactly which versions of their phones have this issue. E.g. from the forum thread:

Can confirm this is still an issue. Also occurs on OnePlus 7 Pro. OnePlus 5T works. It seems to occur due to the inscreen fingerprint sensor. On the Galaxy S10 it does work as well.

The best solution for you here is probably to use the variant of getPassword() and addPassword() with the extra boolean parameter at the end to show android prompt. If true, this will display our own android prompt. On most Android 10 devices this defaults to false, and should so, because passing true here on most Android 10 devices will cause both prompts to appear, and they'll likely overlap.

/**
     * Gets a password from the keychain with option to hide android dialog.  
     * 
     * <p>This will prompt the user to authenticate using biometrics (fingerprint or face recognition).</p>
     * 
     * <p>Note:  If the user adds any new fingerprints or biometric validations to the device, all passwords
     * stored with the old set of fingerprints (etc..) will be invalidated.
     * @param reason Message to display in fingerprint scanning dialog that appears.
     * @param account The account name whose password we wish to obtain.
     * @param showDialogOnAndroid Set false to not show a dialog.  User would still need to use biometrics to authenticate
     * so if you set this to false, you should provide your own dialog on Android.
     * @return AsyncResource Use onResult() or ready() callback to obtain the password, or error condition.  If the password exists in 
     * the keychain, the result will be the password as a string.  If the password doesn't exist, then it will return an error.
     */
    public static AsyncResource<String> getPassword(String reason, String account, boolean showDialogOnAndroid)

/**
     * Adds a password to the keychain.  Some platforms may prompt the user to authenticate
     * to perform this function.  iOS (at least in iOS 13) will not authenticate the user for
     * this action.  It will only authenticate when the user wants to retrieve the password.
     * This is likely because adding authentication at this point wouldn't add any security
     * since the user could just add their fingerprint in "Settings", return to the app
     * and authenticate.  This same loop-hole doesn't exist for getting passwords because
     * passwords are automatically invalidated when adding additional fingerprints to the device.
     * 
     * <p>Enabling authentication for adding passwords on iOS can be achieved by setting 
     * the "ios.Fingerprint.addPassword.prompt" display property to "true"</p>
     * 
     * @param reason A message to display in the fingerprint authentication dialog.
     * @param account The account that the password is for.
     * @param password The password which you wish to store in the keychain.
     * @param showDialogOnAndroid Set false to not show a dialog.  User would still need to use biometrics to authenticate
     * so if you set this to false, you should provide your own dialog on Android.
     * @return AsyncResource that will return success/fail.
     */
    public static AsyncResource<Boolean> addPassword(String reason, String account, String password, boolean showDialogOnAndroid);

So what is the conclusion? Should we call the 'getPassword()' and 'addPassword()' methods with 'showDialogOnAndroid' as 'true' always? Kindly advise.