BiometricPrompt Support in Android
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)
-
-
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"
-
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.
-
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 usingSHA256withRSA
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)
-
Can you list the JCA algs supported by the
AndroidKeyStore
provider?http://www.java2s.com/Code/Java/Security/ListAllProviderAndItsAlgorithms.htm
-
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
. -
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);
andreturn Base64URL.encode(signer.sign())
there’s a need to get user approval for thatSignature
object before the signature bytes can be retrieved. This is done by passing theSignature
object to Android’s BiometricPrompt - and once the user has authenticated, theonAuthenticationSucceeded
method we provide is called asynchronously/sometime later, and only then can we callsigner.sign()
.
-
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.
-
Hallelujah, mystery solved :)
Cheers @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).
-
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
andsign
methods. And then allow the client to resume. Can it be as simple as that? Comments? -
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
andsign
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:
- Get object ready to sign
- Launch task on background thread
- Call nimbus to sign object
- Nimbus calls callback
- callback launches a block on ui thread to do BiometricPrompt operation
- callback blocks it’s thread, waiting on some kind of semaphore/similar threading primitive
- … user completes biometric…
- onAuthentication success is called on ui thread
- onAuthenticationSuccess signals semaphore
- callback wakes up on background thread and returns
- 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 offinishSignature
method the user could to do thesignature.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 )
-
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.
-
reporter I guess we could keep the API by introducing the
getInitiatedSignature
function and by overloading thesign
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()); }
-
- changed title to BiometricPrompt Support in Android
- changed component to JOSE Core
- marked as major
-
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 insidegetInitiatedSignature
.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. )
-
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. -
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 :)
-
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.)
-
Thanks!
Posting a link to the client code from your repo:
https://gitlab.com/emobix/android-biometric-signed-jwt-test/-/blob/1f5f581138c3b5c05d03083f3839bf01db1dac23/app/src/main/java/uk/co/emobix/testapp/MainActivity.java -
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 usualsign
fromJWSSigner
. - The Signature reference is stored as instance variable.
- The
JWSObject.sign
method throws a checked exception, e.g.PromptRequiredException
with theSignature
included. - The client code catches the exception and does its dance with Android Biometrics.
- Finally, the client code calls
sign
again, which uses the storedSignature
reference with thesign(final JWSHeader header, final byte[] signingInput, Signature signer)
method. Alternatively, a newJWSObject.sign
method could be added, intended for completing signing with a givenSignature
. In that case there’s no need to store a reference to it internally.
Thoughts / comments?
-
Let`s make it even crazier!
The
PromptRequiredException
also returns an interface which lets the client code to complete the signing with the clearedSignature
object!No need to extend the
JWSObject
interface!
-
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 anActionRequiredForJWSCompletionException
, 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?
-
Changes: f12fd81
-
- changed status to resolved
Feature was released in v9.4.
Mini guide: https://connect2id.com/products/nimbus-jose-jwt/examples/jws-with-android-biometric-or-pin-prompt
Happy new year everyone!
-
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?
-
Hi Laszlo,
The example was updated to show how a reference to the
Signature
object can be obtain after the key is unlocked:https://connect2id.com/products/nimbus-jose-jwt/examples/jws-with-android-biometric-or-pin-prompt
- Log in to comment
I’m not familiar with this technology.
The example uses a secret key, which is for symmetrical encryption, whereas the RSASSASigner is for digital signatures.
If you’re using a custom key store the operation should work by setting its JCA provider.
Check this out for hints:
https://connect2id.com/products/nimbus-jose-jwt/examples/pkcs11