BiometricPrompt Support in Android

Issue #373 resolved
Miles Stötzner created an issue

Is it possible to use this library in combination with BiometricPrompt? E.g. by passing Signature to RSASSASigner?

I want to sign a JWT using a key that is stored in AndroidKeyStore and that can only be used for a single cryptographic operation after authenticating the user.

Here is an example of how to use the BiometricPrompt for encryption.

Comments (27)

  1. Miles Stötzner reporter

    I dont think that the example applies here. I am already using the nimbus-jose-jwt library in other scenarios to sign JTWs using keys that are hardware backed by AndroidKeyStore (but they do not require user authentication).

    In respect to the BiometicPrompt API I do not want to provide the nimbus-jose-jwt library a key but instead I need to provide the BiometricPromt API a cipher/ signature object that I would like to pass to the nimbus-jose-jwt library. In my understanding the key will be only available for the provided cipher/signature passed to the BiometricPromt API and only if the user gave consent by e.g. fingerprint.

    Here is some code

    // Load AndroidKeyStore
    val keyStore = KeyStore.getInstance("AnroidKeyStore")
    keyStore.load(null)
    
    // Get a reference to the hardware-backed key that requires user authentication
    val key = keyStore.getKey(USER_SIGNATURE_KEY, null)
    
    // Init signature
    val signature = Signature.getInstance("SHA256withRSA")
    signature.initSign(key)
    
    // Pass signature to the BiometricPrompt API
    biometricLoginButton.setOnClickListener {
        biometricPrompt.authenticate(
            promptInfo,
            BiometricPrompt.CryptoObject(signature) 
        )
    }
    
    ...
    
    // Later in a callback iff user e.g. passed fingerprint validation
    
    val signature = result.cryptoObject!!.signature!!
    signature.update( ... some bytes ...)
    val signatureBytes = signature.sign())
    
    
    // If one would directly use the key in the callback without using "signature" it will throw the expection "android.security.KeyStoreException: Key user not authenticated"
    

  2. Vladimir Dzhuvinov

    Hi Miles,

    Could you try the following?

    RSASSASigner signer = new RSASSASigner(key);
    signer.getJCAContext().setProvider(keyStore.getProvider());
    

    The suggested signing pattern is quite unusual.

    The convention with hardware / OS secured keys is to pass the PrivateKey, which in that case is just a handle (no bytes returned when getEncoded() is called) and the java.security.Provider to the RSASSASigner. The RSASSASigner should then automatically instantiate the proper Signature for the Android java.security.Provider.

  3. Miles Stötzner reporter

    Sry, but I am not able to get this running.

    The code below is throwing Unsupported RSASSA algorithm: no such algorithm: SHA256withRSA for provider AndroidKeyStore.

    So there is some kind of misconfiguration since SHA256withRSA is supported (I am actually using SHA256withRSA in other scenarios).

                val keyStore = KeyStore.getInstance("AndroidKeyStore")
                keyStore.load(null)
    
                val key = keyStore.getKey(Crypto.USER_SIGNATURE_KEY, null) as PrivateKey
                val signer = RSASSASigner(key)
                signer.jcaContext.provider = keyStore.provider
    
                val claimsSet = JWTClaimsSet
                    .Builder()
                    .issueTime(Date())
                    .build()
    
                val signedJWT = SignedJWT(
                    JWSHeader
                        .Builder(JWSAlgorithm.RS256)
                        .keyID(Crypto.USER_SIGNATURE_KEY)
                        .build(),
                    claimsSet
    
                )
                signedJWT.sign(signer)
    

  4. Miles Stötzner reporter

    Sry, for the late answer. Here is the output. I am btw using compileSdkVersion 29.

    AndroidKeyStore version 1.0
        KeyFactory.EC
        KeyStore.AndroidKeyStore
        KeyGenerator.HmacSHA384
        KeyGenerator.AES
        Provider.id version
        KeyGenerator.HmacSHA1
        KeyPairGenerator.RSA
        SecretKeyFactory.HmacSHA256
        KeyFactory.RSA
        SecretKeyFactory.HmacSHA224
        KeyGenerator.HmacSHA256
        KeyGenerator.HmacSHA224
        KeyPairGenerator.EC
        SecretKeyFactory.AES
        Provider.id info
        Provider.id className
        SecretKeyFactory.HmacSHA1
        SecretKeyFactory.HmacSHA512
        Provider.id name
        KeyGenerator.HmacSHA512
        SecretKeyFactory.HmacSHA384
    

    No idea why there are no supported algortihms listed. As I already mentioned I am using the AndroidKeyStore for SHA256withRSA.

  5. Joseph Heenan

    Hi all,

    I have just run into the same issue, so perhaps I can expand on what I believe the original issue is, and how I understand the Android Keystore works in this case. (I’m really not following the last few comments on this issue.)

    The Android HSM/TEE can (if you do setUserAuthenticationRequired(true)when creating the key in the HSM) protect a key completely from the OS, to the point where the hardware insists that for every signature operation the user is required to present their biometric. (This is a pretty great thing for securely interacting between mobile devices and a backend, as even if the device is thoroughly compromised, not only is the attacker unable to extract the private key material, they can’t even sign something with the key without the user that owns the relevant finger/other appendage being present, and one biometric presentation from the user results in exactly one signing operation, and definitely no more than one signed object.)

    As Miles said, I believe everything works fine with the AndroidKeyStore and in particular with keys stored in the Android device HSM/TEE, until you use a key where setUserAuthenticationRequired(true) is set.

    What happens currently is you get an exception from Signature.sign():

    android.security.KeyStoreException: Key user not authenticated

    The exception occurs in this piece of code in RSASSASigner.java :

        public Base64URL sign(final JWSHeader header, final byte[] signingInput)
            throws JOSEException {
    
            Signature signer = RSASSA.getSignerAndVerifier(header.getAlgorithm(), getJCAContext().getProvider());
    
            try {
                signer.initSign(privateKey);
                signer.update(signingInput);
                return Base64URL.encode(signer.sign());
    
            } catch (InvalidKeyException e) {
                throw new JOSEException("Invalid private RSA key: " + e.getMessage(), e);
    
            } catch (SignatureException e) {
                throw new JOSEException("RSA signature exception: " + e.getMessage(), e);
            }
        }
    

    In particular my understanding is that in-between signer.update(signingInput);and return Base64URL.encode(signer.sign()) there’s a need to get user approval for that Signature object before the signature bytes can be retrieved. This is done by passing the Signature object to Android’s BiometricPrompt - and once the user has authenticated, the onAuthenticationSucceededmethod we provide is called asynchronously/sometime later, and only then can we call signer.sign().

  6. Miles Stötzner reporter

    yes, i guess that would work. we could split up the function into two functions: the first function returns the initiated signature and the second function takes the signature object returned by the biometric prompt to sign the payload.

  7. Joseph Heenan

    yes, i guess that would work. we could split up the function into two functions: the first function returns the initiated signature and the second function takes the signature object returned by the biometric prompt to sign the payload.

    Yes, exactly.

    For clarity, whilst I have a fair idea how this has to work to use this kind of key on Android, I have no idea how to actually solve the problem. I think these keys can’t be used without some kind of change to the code in nimbus, like splitting the signing process into two separate functions (as Miles says).

  8. Vladimir Dzhuvinov

    Hi Joseph!

    Now that we know what’s going on we can devise a solution. We don’t really do Android here at c2id, so this was really helpful.

    I’m now thinking of allowing client code to set up a callback / hook, and have it invoked between the update and sign methods. And then allow the client to resume. Can it be as simple as that? Comments?

  9. Joseph Heenan

    Hi Vladimir 🙂

    Thank you! Would be great to see a solution for this.

    I’m now thinking of allowing client code to set up a callback / hook, and have it invoked between the update and sign methods. And then allow the client to resume. Can it be as simple as that? Comments?

    I think this would work, although I think it would be relatively awkward for the caller to use.

    I think you’re suggesting you would call a callback/hook, then when that callback returns the operation would continue? (As opposed to the callback returning immediately, then the developer later calling another nimbus method when they’re ready to complete the signing.)

    So if I understand what you’re suggesting correctly, the developer would have to do something like:

    1. Get object ready to sign
    2. Launch task on background thread
    3. Call nimbus to sign object
    4. Nimbus calls callback
    5. callback launches a block on ui thread to do BiometricPrompt operation
    6. callback blocks it’s thread, waiting on some kind of semaphore/similar threading primitive
    7. … user completes biometric…
    8. onAuthentication success is called on ui thread
    9. onAuthenticationSuccess signals semaphore
    10. callback wakes up on background thread and returns
    11. nimbus calls signature.sign() and returns jwt

    It would work I think, but it’s a little annoying to introduce threads to solve this, and it’s a little more complex than I outlined above as we’d also have to make sure to abort the thread when the user fails to complete the biometric.

    The nicer solution (for the developer using nimbus) would be in nimbus returned after signature.update() and nimbus had some kind of finishSignature method the user could to do the signature.sign() and return the jws. (I think this would be quite a big refactoring to achieve though 😞 )

    (I may be missing some nice way of achieving that whole ‘thread’ thing in java. It might be easier using Kotlin coroutines but I guess you don’t want to introduce Kotlin into nimbus 🙂 )

  10. Yavor Vasilev

    We “broke” the API significantly in v9, so maybe it time to do it again, lol

    We’ll do some prototyping to assess the disruption.

  11. Miles Stötzner reporter

    I guess we could keep the API by introducing the getInitiatedSignature function and by overloading the sign function. Just a conceptual suggestion - I am not familiar with the internal structure of nimbus. I also omitted error handling.

    public Signature getInitiatedSignature() {
            Signature signer = RSASSA.getSignerAndVerifier(header.getAlgorithm(), getJCAContext().getProvider());
            return signer.initSign(privateKey);
    }
    
     public Base64URL sign(final JWSHeader header, final byte[] signingInput) {
            return sign(header, signingInput, getSignature())
        }
    
     public Base64URL sign(final JWSHeader header, final byte[] signingInput, Signature signer) {
            signer.update(signingInput);
            return Base64URL.encode(signer.sign());
        }
    

  12. Joseph Heenan

    I may have made a mistake, but my experiments suggested that signer.update() must happen before the BiometricPrompt, so in the code Miles mentions the signer.update(signingInput) must be inside getInitiatedSignature.

    I couldn’t find any clear documentation either way, but when I tried using BiometricPrompt before signer.update() I did get a signature generated, but it jwt.io said it was an invalid signature. (I can’t rule out that I did something stupid and am very happy to concede this point if you have evidence that it should work. 🙂 )

  13. Joseph Heenan

    I think I need to correct some of my above statements - it does seem like doing the BiometricPrompt operation before signer.update() is the correct thing to do. However I’m very puzzled as to why I’m ending up with an apparently invalid signature and I’m still digging into that.

  14. Vladimir Dzhuvinov

    Unfortunately I can’t be of much help here, with Android. I got my first smartphone years after this lib got developed.

    Thanks everyone for the API suggestions. I need to sleep over that first :)

  15. Joseph Heenan

    I was able to figure out the stupid thing I’d done in my code that resulted in an invalid signature, so I’m now confident that my initial message was wrong and the correct timing for doing the biometric authorization is right after initialisation and just before the .update() call. That means Miles’s above suggestion on API would work.

    If it helps I’ve put my hacky test app up here: https://gitlab.com/emobix/android-biometric-signed-jwt-test (it uses jose4j, sorry 🙂 - but jose4j has the same issue and I’ve done something hacky to work around it. The app shows how to create a key that requires a biometric each time and how to do the signing with biometric auth. There’s a README with some hints on how to run it.)

  16. Vladimir Dzhuvinov

    The API addition Miles suggested can become a nice interface extending the current JWSSigner interface. So when JWSObject.sign(JWSSigner) is called by client code it will know whether prompting can be handled or not.

    public interface JWSSignerWithInitiation extends JWSSigner {
    
      Signature getInitiatedSignature();
    
      Base64URL sign(final JWSHeader header, final byte[] signingInput, Signature signer);
    }
    

    Has anyone got other name suggestions for the interface? Speak now 🙂

    Then, how to let the current JWSObject.sign(JWSSigner) signal the client code that prompting is needed? And then complete the signing?

    Here is one super crazy idea - throw an Exception!

    • The JWSSigner instace, e.g. RSASSASigner, is created with a flag to require a prompt.
    • This flag then causes invocation of the getInitiatatedSignature instead of the usual sign from JWSSigner.
    • The Signature reference is stored as instance variable.
    • The JWSObject.sign method throws a checked exception, e.g. PromptRequiredException with the Signature included.
    • The client code catches the exception and does its dance with Android Biometrics.
    • Finally, the client code calls sign again, which uses the stored Signature reference with the sign(final JWSHeader header, final byte[] signingInput, Signature signer) method. Alternatively, a new JWSObject.sign method could be added, intended for completing signing with a given Signature . In that case there’s no need to store a reference to it internally.

    Thoughts / comments?

  17. Vladimir Dzhuvinov

    Let`s make it even crazier!

    The PromptRequiredException also returns an interface which lets the client code to complete the signing with the cleared Signature object!

    No need to extend the JWSObject interface!

  18. Vladimir Dzhuvinov

    We did some prototyping here and got this.

    To get the biometric prompt incorporated the signer needs to be created with an UserAuthenticationRequired option and then, when calling sign, the client code must watch for an ActionRequiredForJWSCompletionException , which returns a Completable interface.

    Everything else remains the same and client code which doesn’t need this Android feature is unaffected.

            // Option to trigger a user authentication prompt after the
            // signature gets initiated
            Set<JWSSignerOption> opts = new HashSet<>();
            opts.add(UserAuthenticationRequired.getInstance());
    
            JWSSigner signer = new RSASSASigner(privateKey, opts);
    
            Payload payload = new Payload("Hello, world!");
    
            JWSObject jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), payload);
    
            ActionRequiredForJWSCompletionException actionRequired = null;
    
            try {
                jwsObject.sign(signer);
            } catch (ActionRequiredForJWSCompletionException arException) {
                // Copy the exception for processing
                actionRequired = arException;
            } catch (JOSEException e) {
                throw new RuntimeException("Signing failed: " + e.getMessage(), e);
            }
    
            assertNotNull(actionRequired);
    
            if (actionRequired != null) {
    
                assertEquals("Authenticate user to complete signing", actionRequired.getMessage());
                assertEquals(UserAuthenticationRequired.getInstance(), actionRequired.getTriggeringOption());
                assertEquals("UserAuthenticationRequired", actionRequired.getTriggeringOption().toString());
    
                // Perform user authentication to unlock the private key, 
                // e.g. with biometric prompt
                // ...
    
                // Complete the signing after the key is unlocked
                actionRequired.getSignCompletion().complete();
            }
    
            assertEquals(JWSObject.State.SIGNED, jwsObject.getState());
    
            String jwsString = jwsObject.serialize();
    

    I’m fairly pleased with the outcome, save for one rough edge (not seen in user code) which I hope we’ll manage to polish.

    The RSxxx and PSxxx algs are covered now. Any interest in the ESxxx algs?

  19. Laszlo Major

    It’s been a couple of years since this was implemented, but I’m not able to find any followup and I’m really struggling to make this work. In the discussion it was mentioned that you need to return the Signature object, so it can be passed to the BiometricPrompt for authenticating, but you ended up with

    // Complete the signing after the key is unlocked
                actionRequired.getSignCompletion().complete();
    

    which detaches from the flow, and the linked guide is a bit of “and then magic happens, and the key is now authenticated”. But, if we don’t have the Signature object to pass into the biometric prompt, the key can not be authenticated for this detached operation.

    Am I missing something here?

  20. Log in to comment