Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AndroidX biometric manager #260

Merged
merged 27 commits into from
Mar 3, 2020

Conversation

OleksandrKucherenko
Copy link
Contributor

@OleksandrKucherenko OleksandrKucherenko commented Oct 21, 2019

implementation of Biometric with fingerprint scan interaction.

Major changes:

  • reused concept of Android Fingerprint #195
  • Added unit tests
  • keychain is a major class for attaching UI interactions. Storage ciphers are low-level classes and should not work with UI
  • made base cipher class that simplifies the implementation of other storages
  • optimized yarn.lock
  • added postinstall action for the sample that attaches the latest source of the library and clean up the folder after that
  • upgraded gradle to 5.6.2 version
  • upgraded RN to 0.61 version
  • added re-packaging code for facebook.conceal library (latest version has problems Conflict with React Native in v2.0.2 facebookarchive/conceal#199)
  • used AndroidX biometric library
  • tested with api19, api21, api23, api28, real device with fingerprints enabled
  • added ability to force usage of specific cipher storage
  • refactored code for future upgrades (passcode, password, pin, etc.)
  • updated documentation, api, ts & js code
  • added several specific for Android flags (build scripts)
  • migrated to latest gradle + build plugin, gradle kotlin (*.kts) scripts
  • example configured as includedBuild (gradle feature)
  • added developer-contributor documentation (in details shows how to setup Android Studio)
  • tested on emulator and devices, compatibility with prev version of stored data added
  • tested upgrade and downgrade secured storage scenarios (documented in readme file)
  • added "warming up" thread that helps java to load all crypto library code in memory and make library works much much faster... instead of 2-12 seconds to access stored secret we spent now 10-30ms.

image

@compojoom
Copy link
Contributor

@OleksandrKucherenko - you haven't modified the example to work on iOS, right?

@OleksandrKucherenko
Copy link
Contributor Author

@compojoom No, only android right now... PR is not finished yet. It's earlier preview.

in plans:

  • dynamic switch between androidx and android support library
  • iOS updates...

@compojoom
Copy link
Contributor

I can do the iOS updates if you want. I wanted to test the example, but it is not working with 0.57 and xcode 11, so I'll have to update it anyway.

@OleksandrKucherenko
Copy link
Contributor Author

@compojoom great! any help is appreciated! Thanks

@compojoom
Copy link
Contributor

compojoom commented Oct 22, 2019

After fighting with it for couple of hours I got it to work. Partially. I guess that I'll need to update the js code as well.

I have a question - I was running into a lot of messages similar to this one:
React-Native: Module RCTLog is not a registered callable module

And it turned out that once I deleted the node_modules/react-native-keychain/node_modules it worked. It seems that since the example install react-native-keychain from ..file if the user runs yarn in the main repository all node files will be also copied to the keychainExample/node_modules/react-native-keychain folder.

I can remove the node_modules in the postinstall_cleanup, but I wonder if that is correct.

@OleksandrKucherenko
Copy link
Contributor Author

@compojoom

I can remove the node_modules in the postinstall_cleanup, but I wonder if that is correct.

The example project is a major test app, that is why I use the postinstall_cleanup.sh script as a post-install step.
In normal life, the post-install script will not make any difference. It's just repeat the .npmignore cleanup logic.

@OleksandrKucherenko
Copy link
Contributor Author

React-Native: Module RCTLog is not a registered callable module

I have no idea why that message shown... maybe a RN 0.6x after migration thing

@compojoom
Copy link
Contributor

here are my changes:
trustlines-network@69a1866

I created a react-native.config.js for autolinking and fixed the keychainExampel - the app runs fine on iOS and on Android. Would you please merge my changes in your branch?

@compojoom
Copy link
Contributor

Do we really need a dynamic switch between androidX and the android support library?

@compojoom
Copy link
Contributor

I tested on an ulefon without any security turned on and I managed to save and load credentials. The phone has faceid support, I turned on this one, but the library was not detecting any faceid.

I looked at the source code and it seems that there is no faceid on android:

  @ReactMethod
  public void getSupportedBiometryType(@NonNull final Promise promise) {
    try {
      boolean fingerprintAuthAvailable = isFingerprintAuthAvailable();

      if (fingerprintAuthAvailable) {
        promise.resolve(FINGERPRINT_SUPPORTED_NAME);
      } else {
        promise.resolve(null);
      }
    } catch (Exception e) {
      Log.e(KEYCHAIN_MODULE, e.getMessage(), e);

      promise.reject(Errors.E_SUPPORTED_BIOMETRY_ERROR, e);
    } catch (Throwable fail) {
      Log.e(KEYCHAIN_MODULE, fail.getMessage(), fail);

      promise.reject(Errors.E_UNKNOWN_ERROR, fail);
    }
  }

Then I tested on a huawei device with fingerprint. I am able to save the password (withot a prompt), but when I try to read I'm presented with the fingerprint input ui. Once I scan the finger however I get an error "could not load credentials. Error: Must be called from main thread of fragment host"

2019-10-23 09:09:04.302 11600-11694/com.keychainexample E/RNKeychainManager: Must be called from main thread of fragment host
    java.lang.IllegalStateException: Must be called from main thread of fragment host
        at androidx.fragment.app.FragmentManagerImpl.ensureExecReady(FragmentManagerImpl.java:1668)
        at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1721)
        at androidx.fragment.app.FragmentManagerImpl.executePendingTransactions(FragmentManagerImpl.java:183)
        at androidx.biometric.BiometricPrompt.authenticateInternal(BiometricPrompt.java:789)
        at androidx.biometric.BiometricPrompt.authenticate(BiometricPrompt.java:662)
        at com.oblador.keychain.KeychainModule$InteractiveBiometric.startAuthentication(KeychainModule.java:569)
        at com.oblador.keychain.KeychainModule$InteractiveBiometric.askAccessPermissions(KeychainModule.java:502)
        at com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb.decrypt(CipherStorageKeystoreRsaEcb.java:139)
        at com.oblador.keychain.KeychainModule.decryptCredentials(KeychainModule.java:306)
        at com.oblador.keychain.KeychainModule.getGenericPasswordForOptions(KeychainModule.java:169)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:371)
        at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:150)
        at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
        at android.os.Handler.handleCallback(Handler.java:907)
        at android.os.Handler.dispatchMessage(Handler.java:105)
        at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:26)
        at android.os.Looper.loop(Looper.java:216)
        at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:225)
        at java.lang.Thread.run(Thread.java:784)

I don't understand however how this is supposed to work cause iOS and android behave slightly different. on iOS if I select passcode, password, touchId or fingerprint. I have to give permission for writing to the keychain.
On android it's writing to the keystore without requesting a permission first, but when I try to read the value - it then asks for permission. is this the way it is supposed to work?

@OleksandrKucherenko
Copy link
Contributor Author

get an error "could not load credentials. Error: Must be called from main thread of fragment host"

Nice catch. I saw that error before, but was not able to catch a clear exception stack trace. Thanks for details. Now I can fix it. It's a 1-2 lines of code.

Several commits from my side should be today to this PR. I almost solve the compatibility issue with androidx/android support libraries (details: https://medium.com/@olku/gradle-androidx-compatibility-3416917b2be1).

@OleksandrKucherenko
Copy link
Contributor Author

OleksandrKucherenko commented Oct 23, 2019

I don't understand however how this is supposed to work cause iOS and android behave slightly different. on iOS if I select passcode, password, touchId or fingerprint. I have to give permission for writing to the keychain.

Yes, true its confusing me too. But that's how its done on android. RSA public key used for encryption, and RSA private key used for decryption. For save operation not needed any "unlock" or "permissions" - just encrypt and save. But for decrypt operation we should allow system to extract private part of the key and its protected by 'fingerprint' scanning process.

In other words "storage" is a SharedPreferences, that is unprotected, it's and XML file inside application folder. But each record inside preferences is encrypted.

FaceId for android can be implemented too, but that will be subject of another PR.

@compojoom
Copy link
Contributor

1-2 lines of code! Damn this is when you know java :) Let me guess - something with new Handler? I've been googling for the past 20mins :)

In the meantime I found a new problem. I'm trying to simulate a fingerprint in the emulator. when I try to save credentials I get: 2019-10-23 09:22:09.709 7638-7732/com.keychainexample E/RNKeychainManager: Unknown error: Attempt to invoke virtual method 'java.security.PublicKey java.security.cert.Certificate.getPublicKey()' on a null object reference

stack trace is

2019-10-23 09:22:09.709 7638-7732/com.keychainexample W/KeyStore: KeyStore exception
    android.os.ServiceSpecificException:  (code 7)
        at android.os.Parcel.createException(Parcel.java:2085)
        at android.os.Parcel.readException(Parcel.java:2039)
        at android.os.Parcel.readException(Parcel.java:1987)
        at android.security.keystore.IKeystoreService$Stub$Proxy.get(IKeystoreService.java:978)
        at android.security.KeyStore.get(KeyStore.java:236)
        at android.security.KeyStore.get(KeyStore.java:225)
        at android.security.keystore.AndroidKeyStoreSpi.engineGetCertificate(AndroidKeyStoreSpi.java:165)
        at java.security.KeyStore.getCertificate(KeyStore.java:1120)
        at com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb.innerEncryptedCredentials(CipherStorageKeystoreRsaEcb.java:201)
        at com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb.encrypt(CipherStorageKeystoreRsaEcb.java:70)
        at com.oblador.keychain.KeychainModule.setGenericPasswordForOptions(KeychainModule.java:136)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:371)
        at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:150)
        at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:26)
        at android.os.Looper.loop(Looper.java:214)
        at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:225)
        at java.lang.Thread.run(Thread.java:919)

@OleksandrKucherenko
Copy link
Contributor Author

1-2 lines of code! Damn this is when you know java :) Let me guess - something with new Handler? I've been googling for the past 20mins :)

    /** trigger interactive authentication. */
    public void startAuthentication() {
      final FragmentActivity activity = (FragmentActivity) getCurrentActivity();
      if (null == activity) throw new NullPointerException("Not assigned current activity");

      // code can be executed only from MAIN thread
      if(Thread.currentThread() != Looper.getMainLooper().getThread()){
        activity.runOnUiThread(this::startAuthentication);
        return;
      }

      final BiometricPrompt prompt = new BiometricPrompt(activity, executor, this);
      final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
        .setTitle("Authentication required")
        .setNegativeButtonText("Cancel")
        .setSubtitle("Please use biometric authentication to unlock the app")
        .build();

      prompt.authenticate(info);
    }
  }

@OleksandrKucherenko
Copy link
Contributor Author

In the meantime I found a new problem. I'm trying to simulate a fingerprint in the emulator. when I try to save credentials I get: 2019-10-23 09:22:09.709 7638-7732/com.keychainexample E/RNKeychainManager: Unknown error: Attempt to invoke virtual method 'java.security.PublicKey java.security.cert.Certificate.getPublicKey()' on a null object reference

You should try to test on real device. Emulators unfortunately are quite bad with fingerprint emulation. You should check API level (should be 23 or higher), and required real crypto libraries (that are often not a part of emulator OR limited in scope)

@compojoom
Copy link
Contributor

Ok, ordered a cheap phone with fingerprint reader. I got another funny error on samsung galaxy s9. I was able to store the credentials, but trying to read them resulted in:

2019-10-23 10:39:05.263 30446-30510/com.keychainexample D/CipherStorageBase: Unlock of keystore is needed. Error: User not authenticated
    android.security.keystore.UserNotAuthenticatedException: User not authenticated
        at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1183)
        at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1245)
        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:2663)
        at javax.crypto.Cipher.tryCombinations(Cipher.java:2570)
        at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2475)
        at javax.crypto.Cipher.chooseProvider(Cipher.java:566)
        at javax.crypto.Cipher.init(Cipher.java:830)
        at javax.crypto.Cipher.init(Cipher.java:771)
        at com.oblador.keychain.cipherStorage.CipherStorageBase$Defaults.lambda$static$1(CipherStorageBase.java:405)
        at com.oblador.keychain.cipherStorage.-$$Lambda$CipherStorageBase$Defaults$gUPKtklt7huSpCITAtk3nzsSgTY.initialize(Unknown Source:0)
        at com.oblador.keychain.cipherStorage.CipherStorageBase.decryptBytes(CipherStorageBase.java:292)
        at com.oblador.keychain.cipherStorage.CipherStorageBase.decryptBytes(CipherStorageBase.java:247)
        at com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb.decrypt(CipherStorageKeystoreRsaEcb.java:127)
        at com.oblador.keychain.KeychainModule.decryptCredentials(KeychainModule.java:306)
        at com.oblador.keychain.KeychainModule.getGenericPasswordForOptions(KeychainModule.java:169)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:371)
        at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:150)
        at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
        at android.os.Handler.handleCallback(Handler.java:789)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:26)
        at android.os.Looper.loop(Looper.java:164)
        at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:225)
        at java.lang.Thread.run(Thread.java:764)
2019-10-23 10:39:05.274 30446-30510/com.keychainexample E/RNKeychainManager: No decryption results and no error. Something deeply wrong!

@OleksandrKucherenko
Copy link
Contributor Author

Ok, ordered a cheap phone with fingerprint reader. I got another funny error on samsung galaxy s9. I was able to store the credentials, but trying to read them resulted in:

ups... that is my issue, result of quick fix with main thread... lib should not release the caller thread untill recieve the result of user interaction.

@compojoom
Copy link
Contributor

Another thing that is strage. With your changes when I try to sync gradle I get:

1: Task failed with an exception.
-----------
* Where:
Build file '/Users/xxxx/react-native-keychain/KeychainExample/node_modules/react-native-keychain/android/build.gradle' line: 12

* What went wrong:
A problem occurred evaluating project ':react-native-keychain'.
> Plugin with id 'com.adarshr.test-logger' not found.

I had to either comment this line out or add:

plugins {
  id "com.adarshr.test-logger" version "2.0.0"
}

to the build.gradle file. Why is it working at your end?

@compojoom
Copy link
Contributor

Just tested your changes and now it seems to work fine!
Tested on Huawei and samsung - it works.

The foldable emulator with API 29 didn't work, but Pixel2 emuulator with API 28 worked as well.

@OleksandrKucherenko
Copy link
Contributor Author

Another thing that is strage. With your changes when I try to sync gradle I get:

fixed... but RN59 example is not finished yet

@OleksandrKucherenko
Copy link
Contributor Author

image

@OleksandrKucherenko
Copy link
Contributor Author

and now RN59

image

@OleksandrKucherenko
Copy link
Contributor Author

@oblador - I think PR is ready for merge. Required only testing on iOS - @compojoom

@aeirola
Copy link
Collaborator

aeirola commented Oct 23, 2019

Hi, thanks for the big effort in building fingerprint support for Android into the library! The PR seems to be quite large, and in many ways overlapping with #195. I'm wondering wether it would be more beneficial to first focus on getting the main fingerprint functionality to work through the #195 PR, and then build on top of that with separate PRs to introduce the changes proposed here. I could at least see some of the changes be separated into separate PRs for easier review:

  • Refactoring Android authentication code, and adding unit tests
  • Updating the example app to 0.61
  • Fixes to Facebook conceal
  • Various development and project structure tweaks
    (not necessarily in that order though)

@aeirola
Copy link
Collaborator

aeirola commented Oct 23, 2019

Do we really need a dynamic switch between androidX and the android support library?

I agree that it probably doesn't make sense to provide backwards compativility to android support library, as release 4.0.0 already moved the library to AndroidX from android support library.

@OleksandrKucherenko
Copy link
Contributor Author

I agree that it probably doesn't make sense to provide backwards compativility to android support library,

it's done. Please review and approve it.

@compojoom
Copy link
Contributor

@OleksandrKucherenko - I haven't tested the lateste changes yet, but just from the review - your KeychainExample is the old version - you haven't applied my changes to it and it was not working yesterday. So I doubt that it will work today. Or am I missing something?
And you are missing the react-native.config.js file for autolinking on RN 0.61. Check my modifications again please.

@OleksandrKucherenko
Copy link
Contributor Author

PR is temporary not usable... I'm working on gradle fix.

workaround:

# cd KeychainExample/android
./gradlew preBuild
./graldew assembleDebug

@oblador
Copy link
Owner

oblador commented Oct 25, 2019

Just weighing in on the backwards compatibility parts; since Google has a requirement for people to migrate towards AndroidX and RN 0.60 is necessary to do so, I don't see the point in keeping backwards compatibility with 0.59. I'm fine with having 0.61 as a requirement even, just have to follow semver and release it as a new major version.

added strings for copy/paste

fixed anchors

cleanup the code

iOS fix, documentation become more accurate now

improved ios part

last minute changes

updated list of ignored files

Sync api method changes

cleanup
@oblador oblador merged commit c854720 into oblador:master Mar 3, 2020
@jenskuhrjorgensen
Copy link

Wohooo 😄 🍾 🎉
Super excited to try it out!

@tunm1228
Copy link

tunm1228 commented May 7, 2021

i installed verison 7.0.0 but Android not authenticate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.