- changed status to open
JWE with shared key support for Android Hardware KeyStore (TEE) keys
Nimbus has existing support for JWE with shared key encryption as per example at https://connect2id.com/products/nimbus-jose-jwt/examples/jwe-with-shared-key
The example itself works well.
But the flow detailed there fails to work with AES keys stored in Android Hardware KeyStore (or in any TEE / HSM for that matter).
This is mostly because the Nimbus library attempts to directly access and use the AES key bytes. But the bytes are not accessible in case of a TEE (or similar system). All key operations must go through the TEE itself.
The following problems occur if either encryption or decryption is used via DirectEncrypter
or DirectDecrypter
Some examples:
-
Both: As soon as the constructor is called the parent class DirectCryptoProvider attemps to read the AES key bytes and get the length of the key. This fails as the key is in a TEE and key bytes are not directly available.
-
DirectEncrypter: Same issue at the start of encryption: https://bitbucket.org/connect2id/nimbus-jose-jwt/src/d8a4358e56328637974b343a0834da67e20a6e30/src/main/java/com/nimbusds/jose/crypto/DirectEncrypter.java#lines-139 This fails as the key is in a TEE and key bytes are not directly available.
- DirectEncryptor: When A128CBC_HS256 or similar is used then the AESCBC class attempts to divide the original key into a composite key https://bitbucket.org/connect2id/nimbus-jose-jwt/src/d8a4358e56328637974b343a0834da67e20a6e30/src/main/java/com/nimbusds/jose/crypto/impl/AESCBC.java#lines-191 This fails as the key is in a TEE and key bytes are not directly available.
- DirectDecryptor: Same problem as above during decryption.
I am assuming these same things would be an issue in the case of any HSM usage. And not only with Android TEE keys.
I am wondering if there have been any discussions of supporting hardware such as Android TEE?
Some background - my own use case is related to exchanging messages between backend and Android client using a shared AES key imported to the client using https://developer.android.com/training/articles/keystore#ImportingEncryptedKeys
The import part works well for the devices with import support in my POC. And the JWE a with shared key seems like the appropriate way to use that shared key for more secure communication.
PS: I see Nimbus has had an effort to introduce some support for Android KeyStore-related functionality already. Like https://connect2id.com/products/nimbus-jose-jwt/examples/jws-with-android-biometric-or-pin-prompt
Comments (23)
-
-
- changed component to Crypto package
-
reporter Once those checks are smartened so they don't get into the way, I suppose the current AES/GCM cipher will work just as it is.
The current code for the AES/CBC ciphers (the one you were referring to) will likely need a big rework.
Good point. The AES/CBC would most likely need a bigger rework. I’ll investigate if AES/GCM is an option for us.
Would you post the stack trace you get when the code tries to get the key length?
Sure. Regardless if I pick AES/CBC or AES/GCM the first issue is right at the creation of
DirectDecrypter
Server-side - no HSM here atm, key bytes accessible
override fun encryptMessageWithTekToJWE(message: String, tekAesKeyAtServer: SecretKeySpec): String { // Create the header // (“enc”=”A128CBC-HS256”, “alg”=”dir”), val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A128CBC_HS256) // Set the plain text val payload = Payload(message) // Create the JWE object and encrypt it val jweObject = JWEObject(header, payload) jweObject.encrypt(DirectEncrypter(tekAesKeyAtServer)) // Serialise to compact JOSE form... return jweObject.serialize() }
Client-side “CryptoClient“ - Uses the same AES key, but in Android TEE Strongbox (https://developer.android.com/training/articles/keystore#HardwareSecurityModule)
override fun decryptJWEWithImportedWrappedKey(keyStoreKeyAlias: String, messageWrappedTekEncryptedJWE: String): String { val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null, null) val secretKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.SecretKeyEntry val decrypter = DirectDecrypter(secretKeyEntry.secretKey) // <- Throws from here decrypter.jcaContext.provider = keyStore.provider val jweObject = JWEObject.parse(messageWrappedTekEncryptedJWE) jweObject.decrypt(decrypter) return jweObject.payload.toString() }
And the stack:
com.nimbusds.jose.KeyLengthException: The Content Encryption Key length must be 128 bits (16 bytes), 192 bits (24 bytes), 256 bits (32 bytes), 384 bits (48 bytes) or 512 bites (64 bytes) at com.nimbusds.jose.crypto.impl.DirectCryptoProvider.getCompatibleEncryptionMethods(DirectCryptoProvider.java:98) at com.nimbusds.jose.crypto.impl.DirectCryptoProvider.<init>(DirectCryptoProvider.java:124) at com.nimbusds.jose.crypto.DirectDecrypter.<init>(DirectDecrypter.java:129) at com.nimbusds.jose.crypto.DirectDecrypter.<init>(DirectDecrypter.java:103) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.decryptJWEWithImportedWrappedKey(CryptoClient.kt:128) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.runTestDecryptJweWithImportedKeyAtClient(KeyImportUseCase.kt:161) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.runTest(KeyImportUseCase.kt:115) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.execute$lambda-0(KeyImportUseCase.kt:28) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.$r8$lambda$4y8e3eb9AeJa7FYDsQFUD3kuaQA(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase$$ExternalSyntheticLambda0.call(Unknown Source:4) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012)
Would you be able to get a list of the supported ciphers in your Android / TEE env
Sure. The supported algorithms depend on the hardware and API levels. But an overview is at https://developer.android.com/training/articles/keystore#SupportedCiphers
Btw, I do have a simple android demo app that does the whole key import and then attempts to use the key. And displays results. The “server“ part is also done in the same app for convenience so running it is just I button click
I have to check with the team but most likely I could move this to open-source. Then you can directly access and edit the methods described above. If that would help you and it is something you are interested in then I’ll look into this.
-
Updates the DirectCryptoProvider (alg=dir) to support HSM-based SecretKey instances with A128GCM, A192GCM and A256GCM: 23cd918143126c3538350fd2c746cf0073f05d1e
-
I pushed the above patch as
9.25.1 (2022-09-20)
to Maven Central.Check it out and let me know if it works for the AES/GCM family of enc algs.
Note, AES/CBC/HMAC is not supported by this patch. It will need a more substantial re-engineering and I’m not sure it can be made to work with an HSM due to the nature of the key operations.
-
reporter Hei!
Awesome news! Thanks!
I will try it as soon as possible and will get back to you
Btw, I moved my whole test app to open source: https://github.com/LabMobi/HardwareKeyImportDemoAndroid
-
reporter Hei again!
I tried with DirectEncryptor and DIR and A256GCM using the
9.25.1
version.Still a small issue with the decryption part:
- The code at
toAESKey()
method https://bitbucket.org/connect2id/nimbus-jose-jwt/src/005065d06f06de859a5853c871e711474bbc1d0f/src/main/java/com/nimbusds/jose/util/KeyUtils.java#lines-48 accesses the key bytes - That method is called at
decrypt()
method at https://bitbucket.org/connect2id/nimbus-jose-jwt/src/005065d06f06de859a5853c871e711474bbc1d0f/src/main/java/com/nimbusds/jose/crypto/impl/AESGCM.java#lines-270 - That and that is in turn called from
decrypt()
method at https://bitbucket.org/connect2id/nimbus-jose-jwt/src/005065d06f06de859a5853c871e711474bbc1d0f/src/main/java/com/nimbusds/jose/crypto/impl/ContentCryptoProvider.java#lines-298 - And that is called from DirectDecrypter at https://bitbucket.org/connect2id/nimbus-jose-jwt/src/005065d06f06de859a5853c871e711474bbc1d0f/src/main/java/com/nimbusds/jose/crypto/DirectDecrypter.java#lines-272
Resulting in a stack trace of:
onKeyImportTestFailed: com.nimbusds.jose.JOSEException: Missing argument at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:434) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.decryptJWEWithImportedWrappedKey(CryptoClient.kt:234) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.runTestDecryptJweWithImportedKeyAtClient(KeyImportUseCase.kt:159) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.runTest(KeyImportUseCase.kt:115) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.execute$lambda-0(KeyImportUseCase.kt:29) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.$r8$lambda$4y8e3eb9AeJa7FYDsQFUD3kuaQA(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase$$ExternalSyntheticLambda0.call(Unknown Source:4) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.lang.IllegalArgumentException: Missing argument at javax.crypto.spec.SecretKeySpec.<init>(SecretKeySpec.java:93) at com.nimbusds.jose.util.KeyUtils.toAESKey(KeyUtils.java:48) at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:270) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.decrypt(ContentCryptoProvider.java:298) at com.nimbusds.jose.crypto.DirectDecrypter.decrypt(DirectDecrypter.java:272) at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:420) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.decryptJWEWithImportedWrappedKey(CryptoClient.kt:234) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.runTestDecryptJweWithImportedKeyAtClient(KeyImportUseCase.kt:159) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.runTest(KeyImportUseCase.kt:115) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.execute$lambda-0(KeyImportUseCase.kt:29) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase.$r8$lambda$4y8e3eb9AeJa7FYDsQFUD3kuaQA(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase$$ExternalSyntheticLambda0.call(Unknown Source:4) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012)
NOTE: I believe the same issue is when I would use the DirectEncrypter with the TEE.
Feel free to try it yourself from the “feature/1-use-aesgcm-for-the-jwe-encrypted-message” branch of https://github.com/LabMobi/HardwareKeyImportDemoAndroid
- The encryption is done here without TEE (this is the “serverside”): https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/feature/1-use-aesgcm-for-the-jwe-encrypted-message/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt#L145
- The decryption is done here: https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/feature/1-use-aesgcm-for-the-jwe-encrypted-message/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt#L225
- The code at
-
reporter Small update to the demo code:
-
The Demo at https://github.com/LabMobi/HardwareKeyImportDemoAndroid now tries both communication directions:
- Server software key encryption → client TEE key decryption. This has the problem described above
- Client TEE key encryption → server software key encryption. This also fails on the TEE part, see below
-
Separated the key import and key usage tests so both can be run separately
The trace if DirectEncrypter is used with TEE key:
com.nimbusds.jose.JOSEException: Missing argument at com.nimbusds.jose.JWEObject.encrypt(JWEObject.java:385) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.encryptMessageWithTekToJWE(CryptoClient.kt:185) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTestClientEncryptCryptAndServerDecryptJweWithTek(ImportedKeyTwoWayUsageUseCase.kt:83) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTest(ImportedKeyTwoWayUsageUseCase.kt:34) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.execute$lambda-0(ImportedKeyTwoWayUsageUseCase.kt:25) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.$r8$lambda$AzX65FVi6jZ5MMfX4wL4a-VFvC4(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase$$ExternalSyntheticLambda0.call(Unknown Source:6) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.lang.IllegalArgumentException: Missing argument at javax.crypto.spec.SecretKeySpec.<init>(SecretKeySpec.java:93) at com.nimbusds.jose.util.KeyUtils.toAESKey(KeyUtils.java:48) at com.nimbusds.jose.crypto.impl.AESGCM.encrypt(AESGCM.java:107) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.encrypt(ContentCryptoProvider.java:203) at com.nimbusds.jose.crypto.DirectEncrypter.encrypt(DirectEncrypter.java:137) at com.nimbusds.jose.JWEObject.encrypt(JWEObject.java:375) ... 16 more com.nimbusds.jose.JOSEException: Missing argument at com.nimbusds.jose.JWEObject.encrypt(JWEObject.java:385) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.encryptMessageWithTekToJWE(CryptoClient.kt:185) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTestClientEncryptCryptAndServerDecryptJweWithTek(ImportedKeyTwoWayUsageUseCase.kt:83) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTest(ImportedKeyTwoWayUsageUseCase.kt:34) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.execute$lambda-0(ImportedKeyTwoWayUsageUseCase.kt:25) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.$r8$lambda$AzX65FVi6jZ5MMfX4wL4a-VFvC4(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase$$ExternalSyntheticLambda0.call(Unknown Source:6) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.lang.IllegalArgumentException: Missing argument at javax.crypto.spec.SecretKeySpec.<init>(SecretKeySpec.java:93) at com.nimbusds.jose.util.KeyUtils.toAESKey(KeyUtils.java:48) at com.nimbusds.jose.crypto.impl.AESGCM.encrypt(AESGCM.java:107) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.encrypt(ContentCryptoProvider.java:203) at com.nimbusds.jose.crypto.DirectEncrypter.encrypt(DirectEncrypter.java:137) at com.nimbusds.jose.JWEObject.encrypt(JWEObject.java:375) ... 16 more
NOTE: If you do not have an Android device that supports the key import then I guess the code does not help you much. Then let me know and I’ll see if I can make a debug option to just generate the key directly in the TEE without the import so you can use this code to test. Or just let me know when you have a new version and I’ll test and report back :)
Thanks!
-
-
The key utility is updated now, to wrap the input key instead of recreating it: e6d6f221
Check out the new
version 9.25.2 (2022-09-22)
and let me know if it works now.I wanted to add tests that will fail if the code calls the
getEncoded
method when it shouldn't (for an HSM), rather than trying to work out some Android integration tests (and we don't have any Android developers here, only backend :) ) -
reporter I wanted to add tests that will fail if the code calls the
getEncoded
method when it shouldn't (for an HSM), rather than trying to work out some Android integration tests (and we don't have any Android developers here, only backend :) )Understandable. That works for me.
Now that AES key creation part is fine. Thanks!
I am running into a strange issue which maybe is a platform-related one. Just not sure what is going wrong there.
com.nimbusds.jose.JOSEException: Couldn't create AES/GCM/NoPadding cipher: Provider AndroidKeyStore does not provide AES/GCM/NoPadding at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:286) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.decrypt(ContentCryptoProvider.java:298) at com.nimbusds.jose.crypto.DirectDecrypter.decrypt(DirectDecrypter.java:272) at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:420) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.decryptJWEWithImportedKey(CryptoClient.kt:167) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTestServerEncryptCryptAndClientDecryptJweWithTek(ImportedKeyTwoWayUsageUseCase.kt:70) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTest(ImportedKeyTwoWayUsageUseCase.kt:34) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.execute$lambda-0(ImportedKeyTwoWayUsageUseCase.kt:25) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.$r8$lambda$AzX65FVi6jZ5MMfX4wL4a-VFvC4(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase$$ExternalSyntheticLambda0.call(Unknown Source:6) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.security.NoSuchAlgorithmException: Provider AndroidKeyStore does not provide AES/GCM/NoPadding at javax.crypto.Cipher.createCipher(Cipher.java:739) at javax.crypto.Cipher.getInstance(Cipher.java:718) at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:276) ... 19 more
This “
Provider AndroidKeyStore does not provide AES/GCM/NoPadding
“ directly contradicts the API documentation here: https://developer.android.com/training/articles/keystore#SupportedCiphersI also double-checked the IV and it is the 12 bytes as required. I also made sure the devices I tested on were originally released with higher API levels than the required API 23 (Pixel 3 and Pixel 6 Pro).
Then I remove the provider specification from:
val decrypter = DirectDecrypter(secretKeyEntry.secretKey) // DO NOT SET: decrypter.jcaContext.provider = keyStore.provider
then the process dies somewhere in Bouncy Castle library:
com.nimbusds.jose.JOSEException: Attempt to get length of null array at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:434) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.decryptJWEWithImportedKey(CryptoClient.kt:167) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTestServerEncryptCryptAndClientDecryptJweWithTek(ImportedKeyTwoWayUsageUseCase.kt:70) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTest(ImportedKeyTwoWayUsageUseCase.kt:34) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.execute$lambda-0(ImportedKeyTwoWayUsageUseCase.kt:25) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.$r8$lambda$AzX65FVi6jZ5MMfX4wL4a-VFvC4(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase$$ExternalSyntheticLambda0.call(Unknown Source:6) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.lang.NullPointerException: Attempt to get length of null array at com.android.org.bouncycastle.crypto.params.KeyParameter.<init>(KeyParameter.java:17) at com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineInit(BaseBlockCipher.java:787) at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2981) at javax.crypto.Cipher.tryCombinations(Cipher.java:2892) at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2797) at javax.crypto.Cipher.chooseProvider(Cipher.java:774) at javax.crypto.Cipher.init(Cipher.java:1289) at javax.crypto.Cipher.init(Cipher.java:1224) at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:282) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.decrypt(ContentCryptoProvider.java:298) at com.nimbusds.jose.crypto.DirectDecrypter.decrypt(DirectDecrypter.java:272) at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:420) ... 16 more
Why I am wondering about the explicit provider - the following works fine with a TEE hardware key: https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/master/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt#L165 And no provider needed to be specified there.
-
reporter OK, when I take the working code from https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/master/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt#L165 and also set the provider there:
override fun decryptTextWithImportedKey(keyStoreKeyAlias: String, messageTekEncryptedAtClient: ByteArray): String { val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null, null) val key: SecretKey = keyStore.getKey(keyStoreKeyAlias, null) as SecretKey val c: Cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", keyStore.provider) // First 16 byte are iv val ivPart: ByteArray = messageTekEncryptedAtClient.copyOfRange(0, 16) val messagePart: ByteArray = messageTekEncryptedAtClient.copyOfRange(16, messageTekEncryptedAtClient.size) val ivParamSpec = IvParameterSpec(ivPart) c.init(Cipher.DECRYPT_MODE, key, ivParamSpec) return String(c.doFinal(messagePart)) }
then this ends in the same - methods starts throwing with “java.security.NoSuchAlgorithmException: Provider AndroidKeyStore does not provide AES/CBC/PKCS7Padding”. Which seems absurd,
java.security.NoSuchAlgorithmException: Provider AndroidKeyStore does not provide AES/CBC/PKCS7Padding at javax.crypto.Cipher.createCipher(Cipher.java:739) at javax.crypto.Cipher.getInstance(Cipher.java:718) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.decryptTextWithImportedKey(CryptoClient.kt:165) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyLocalUsageUseCase.runTestCryptAndEncryptTextWithTek(ImportedKeyLocalUsageUseCase.kt:51) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyLocalUsageUseCase.runTest(ImportedKeyLocalUsageUseCase.kt:30) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyLocalUsageUseCase.execute$lambda-0(ImportedKeyLocalUsageUseCase.kt:21) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyLocalUsageUseCase.$r8$lambda$Z09gienJI1Qj4cbzTOW_lDMqT38(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyLocalUsageUseCase$$ExternalSyntheticLambda0.call(Unknown Source:4) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012)
The official guide also does not say I would need to set the provider. In fact, this is discouraged: https://developer.android.com/guide/topics/security/cryptography#bc-algorithms
-
reporter One more interesting fact - when I try to set the same provider Cipher uses by default then I get again an issue with the missing key bytes:
override fun decryptJWEWithImportedKey(keyStoreKeyAlias: String, messageWrappedTekEncryptedJWE: String): String { val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) keyStore.load(null, null) val secretKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.SecretKeyEntry val decrypter = DirectDecrypter(secretKeyEntry.secretKey) // Set the same provider that Cipher.getInstance would get decrypter.jcaContext.provider = Cipher.getInstance("AES/GCM/NoPadding").provider val jweObject = JWEObject.parse(messageWrappedTekEncryptedJWE) jweObject.decrypt(decrypter) return jweObject.payload.toString() }
and this results in:
com.nimbusds.jose.JOSEException: Couldn't create AES/GCM/NoPadding cipher: key.getEncoded() == null at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:286) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.decrypt(ContentCryptoProvider.java:298) at com.nimbusds.jose.crypto.DirectDecrypter.decrypt(DirectDecrypter.java:272) at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:420) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.decryptJWEWithImportedKey(CryptoClient.kt:167) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTestServerEncryptCryptAndClientDecryptJweWithTek(ImportedKeyTwoWayUsageUseCase.kt:70) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTest(ImportedKeyTwoWayUsageUseCase.kt:34) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.execute$lambda-0(ImportedKeyTwoWayUsageUseCase.kt:25) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.$r8$lambda$AzX65FVi6jZ5MMfX4wL4a-VFvC4(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase$$ExternalSyntheticLambda0.call(Unknown Source:6) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.security.InvalidKeyException: key.getEncoded() == null at com.android.org.conscrypt.OpenSSLCipher.checkAndSetEncodedKey(OpenSSLCipher.java:478) at com.android.org.conscrypt.OpenSSLCipher.engineInit(OpenSSLCipher.java:307) at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2981) at javax.crypto.Cipher.tryCombinations(Cipher.java:2878) at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2797) at javax.crypto.Cipher.chooseProvider(Cipher.java:774) at javax.crypto.Cipher.init(Cipher.java:1289) at javax.crypto.Cipher.init(Cipher.java:1224) at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:282) ... 19 more
-
reporter Hmm, maybe there is still an issue with the extra wrapping of the key? The last change you made.
Because when I use a debugger and check what is the key type received in the DirectEncrypter’s constructor then that is “class android.security.keystore2.AndroidKeyStoreSecretKey“:
-
I realised the
KeyUtil.toAESKey
must return an AES key unmodified for an HSM, else the HSM will lose its internal reference to the key, so when the Cipher is called it won’t know which internal key representation to use.Here is the fix: f9ee60b0f5d759097024c97745f7826a23fe1964
Get the latest
version 9.25.3 (2022-09-24)
and give it another try. Fingers crossed -
reporter Thanks!
Seems that your last change helped.
Now when I do not set the provider directly (a strategy that works when I do not use Nimbus):
override fun decryptJWEWithImportedKey(keyStoreKeyAlias: String, messageWrappedTekEncryptedJWE: String): String { val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) keyStore.load(null, null) val secretKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.SecretKeyEntry val decrypter = DirectDecrypter(secretKeyEntry.secretKey) //Do not use this: decrypter.jcaContext.provider = keyStore.provider val jweObject = JWEObject.parse(messageWrappedTekEncryptedJWE) jweObject.decrypt(decrypter) return jweObject.payload.toString() }
then it seems to go to the correct keystore / implementation.
But not full success yet :)
I get this padding /
Incompatible block mode
issue:com.nimbusds.jose.JOSEException: Couldn't create AES/GCM/NoPadding cipher: Keystore operation failed at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:286) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.decrypt(ContentCryptoProvider.java:298) at com.nimbusds.jose.crypto.DirectDecrypter.decrypt(DirectDecrypter.java:272) at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:420) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.decryptJWEWithImportedKey(CryptoClient.kt:167) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTestServerEncryptCryptAndClientDecryptJweWithTek(ImportedKeyTwoWayUsageUseCase.kt:70) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTest(ImportedKeyTwoWayUsageUseCase.kt:34) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.execute$lambda-0(ImportedKeyTwoWayUsageUseCase.kt:25) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.$r8$lambda$AzX65FVi6jZ5MMfX4wL4a-VFvC4(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase$$ExternalSyntheticLambda0.call(Unknown Source:6) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.security.InvalidKeyException: Keystore operation failed at android.security.keystore2.KeyStoreCryptoOperationUtils.getInvalidKeyException(KeyStoreCryptoOperationUtils.java:130) at android.security.keystore2.KeyStoreCryptoOperationUtils.getExceptionForCipherInit(KeyStoreCryptoOperationUtils.java:154) at android.security.keystore2.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:339) at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:234) at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2981) at javax.crypto.Cipher.tryCombinations(Cipher.java:2892) at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2797) at javax.crypto.Cipher.chooseProvider(Cipher.java:774) at javax.crypto.Cipher.init(Cipher.java:1289) at javax.crypto.Cipher.init(Cipher.java:1224) at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:282) ... 19 more Caused by: android.security.KeyStoreException: Incompatible block mode at android.security.KeyStore2.getKeyStoreException(KeyStore2.java:356) at android.security.KeyStoreSecurityLevel.createOperation(KeyStoreSecurityLevel.java:120) at android.security.keystore2.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:334) ... 27 more com.nimbusds.jose.JOSEException: Couldn't create AES/GCM/NoPadding cipher: Keystore operation failed at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:286) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.decrypt(ContentCryptoProvider.java:298) at com.nimbusds.jose.crypto.DirectDecrypter.decrypt(DirectDecrypter.java:272) at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:420) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.decryptJWEWithImportedKey(CryptoClient.kt:167) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTestServerEncryptCryptAndClientDecryptJweWithTek(ImportedKeyTwoWayUsageUseCase.kt:70) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTest(ImportedKeyTwoWayUsageUseCase.kt:34) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.execute$lambda-0(ImportedKeyTwoWayUsageUseCase.kt:25) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.$r8$lambda$AzX65FVi6jZ5MMfX4wL4a-VFvC4(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase$$ExternalSyntheticLambda0.call(Unknown Source:6) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.security.InvalidKeyException: Keystore operation failed at android.security.keystore2.KeyStoreCryptoOperationUtils.getInvalidKeyException(KeyStoreCryptoOperationUtils.java:130) at android.security.keystore2.KeyStoreCryptoOperationUtils.getExceptionForCipherInit(KeyStoreCryptoOperationUtils.java:154) at android.security.keystore2.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:339) at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:234) at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2981) at javax.crypto.Cipher.tryCombinations(Cipher.java:2892) at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2797) at javax.crypto.Cipher.chooseProvider(Cipher.java:774) at javax.crypto.Cipher.init(Cipher.java:1289) at javax.crypto.Cipher.init(Cipher.java:1224) at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:282) ... 19 more Caused by: android.security.KeyStoreException: Incompatible block mode at android.security.KeyStore2.getKeyStoreException(KeyStore2.java:356) at android.security.KeyStoreSecurityLevel.createOperation(KeyStoreSecurityLevel.java:120) at android.security.keystore2.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:334) ... 27 more
Now, this error is a little bit our of my knowledge base. Any ideas here?
PS: When I do set the
decrypter.jcaContext.provider = keyStore.provider
then I get the same as before:
java.security.NoSuchAlgorithmException: Provider AndroidKeyStore does not provide AES/CBC/PKCS7Padding
But for this we know it does not also work when I do not use Nimbus and it is not recommended by Google anyways.
-
reporter As a follow-up to my last message - when I try the other direction - TEE key encrypt → server decrypt then I get an error about IV:
override fun encryptMessageWithTekToJWE(message: String, keyStoreKeyAlias: String): String { val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) keyStore.load(null, null) val secretKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.SecretKeyEntry val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM) // Set the message as payload plain text val payload = Payload(message) // Create the JWE object and encrypt it val jweObject = JWEObject(header, payload) val encrypter = DirectEncrypter(secretKeyEntry.secretKey) // Google does not recommend this: encrypter.jcaContext.provider = keyStore.provider jweObject.encrypt(encrypter) // Serialise to compact JOSE form return jweObject.serialize() }
com.nimbusds.jose.JOSEException: Couldn't create AES/GCM/NoPadding cipher: Caller-provided IV not permitted at com.nimbusds.jose.crypto.impl.AESGCM.encrypt(AESGCM.java:125) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.encrypt(ContentCryptoProvider.java:203) at com.nimbusds.jose.crypto.DirectEncrypter.encrypt(DirectEncrypter.java:137) at com.nimbusds.jose.JWEObject.encrypt(JWEObject.java:375) at mobi.lab.keyimportdemo.infrastructure.crypto.CryptoClient.encryptMessageWithTekToJWE(CryptoClient.kt:185) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTestClientEncryptCryptAndServerDecryptJweWithTek(ImportedKeyTwoWayUsageUseCase.kt:81) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.runTest(ImportedKeyTwoWayUsageUseCase.kt:38) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.execute$lambda-0(ImportedKeyTwoWayUsageUseCase.kt:25) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase.$r8$lambda$AzX65FVi6jZ5MMfX4wL4a-VFvC4(Unknown Source:0) at mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase$$ExternalSyntheticLambda0.call(Unknown Source:6) at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43) at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4855) at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38) at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:25) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012) Caused by: java.security.InvalidAlgorithmParameterException: Caller-provided IV not permitted at android.security.keystore2.KeyStoreCryptoOperationUtils.getExceptionForCipherInit(KeyStoreCryptoOperationUtils.java:150) at android.security.keystore2.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:339) at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:234) at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2981) at javax.crypto.Cipher.tryCombinations(Cipher.java:2892) at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2797) at javax.crypto.Cipher.chooseProvider(Cipher.java:774) at javax.crypto.Cipher.init(Cipher.java:1289) at javax.crypto.Cipher.init(Cipher.java:1224) at com.nimbusds.jose.crypto.impl.AESGCM.encrypt(AESGCM.java:121) ... 19 more
Android’s own documentation says here:
Only 12-byte long IVs supported.
https://developer.android.com/training/articles/keystore#SupportedCiphers
This seems to be what Nimbus does: https://bitbucket.org/connect2id/nimbus-jose-jwt/src/005065d06f06de859a5853c871e711474bbc1d0f/src/main/java/com/nimbusds/jose/crypto/impl/AESGCM.java#lines-71
If I am reading the stack correctly then it ways we can’t specific IV ourselves at all?
Caused by: java.security.InvalidAlgorithmParameterException: Caller-provided IV not permitted
There is a blog post that suggests the same here:
Unfortunately, if we try to execute it we will end up with an exception:
java.security.InvalidAlgorithmParameterException: Caller-provided IV not permitted
— which is, I believe, quite self-explanatory: Android does not allow us to specify and IV. Period.But, why? Well, this is a behaviour introduced in API 23 and is meant to make sure that the caller is not reusing IVs, since this could break the security of the block cipher (as I explained before). The bottom line is: you should not reuse IVs with the same key, and in order to make sure of that Android is generating a random one internally.
https://levelup.gitconnected.com/doing-aes-gcm-in-android-adventures-in-the-field-72617401269d
-
Regarding the
Couldn't create AES/GCM/NoPadding cipher: Keystore operation failed
- what is the length of the AES key that you have? Does it match the “enc”? E.g. 128 bits for theA128GCM
enc? The AES block length must be matched by the key length.Thanks for the IV article. I checked it out and understood what Android does there. The Nimbus lib will need some kind of a switch to disable external IVs (the default mode). I was thinking something along the lines of a
JWEEncrypterOption
:https://connect2id.com/products/nimbus-jose-jwt/examples/jws-with-android-biometric-or-pin-prompt
-
reporter Thanks for the suggestion.
I double-checked the key sizes. And made the code a little bit more clear where the values are in bits and where in bytes. But seems these values are correct. I do generate and import 256-bit AES key. And then specify the enc value to
EncryptionMethod.A256GCM
.- Key size is defined here: https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/feature/1-use-aesgcm-for-the-jwe-encrypted-message/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/DomainConstants.kt#L4
- Key generation for import is here: https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/feature/1-use-aesgcm-for-the-jwe-encrypted-message/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt#L39
- The imported key size in import metadata is here: https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/feature/1-use-aesgcm-for-the-jwe-encrypted-message/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt#L58
- EncryptionMehtod selection is here: https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/feature/1-use-aesgcm-for-the-jwe-encrypted-message/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoUtil.kt#L25
- The selection is set here: https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/feature/1-use-aesgcm-for-the-jwe-encrypted-message/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt#L167
I also just in case tested by replacing the TEE decrypt part with Java one. And it decrypts just fine with the same pre-import key. For now, it looks like the key size is not the problem.
But maybe it is the cryptogram size?
While updating the code I noticed this “
// Remove GCM tag from end of output
“ from the key import code.This is originally written by Google:
// Encrypt secure key Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec secretKeySpec = new SecretKeySpec(aesKeyBytes, "AES"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_SIZE, iv); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec); byte[] aad = wrappedKeyDescription.getEncoded(); cipher.updateAAD(aad); byte[] encryptedSecureKey = cipher.doFinal(keyMaterial); // Get GCM tag. Java puts the tag at the end of the ciphertext data :( int len = encryptedSecureKey.length; int tagSize = (GCM_TAG_SIZE / 8); byte[] tag = Arrays.copyOfRange(encryptedSecureKey, len - tagSize, len); // Remove GCM tag from end of output encryptedSecureKey = Arrays.copyOfRange(encryptedSecureKey, 0, len - tagSize);
Could this be the issue?
If I use GCM256 in Java it puts the tag at the end of the resulting encrypted value? And Android TEE does not expect it there? And that is why the
android.security.KeyStoreException: Incompatible block mode
?Or is this just an undocumented “feature“ of the key import part ..
-
Another fine example why practising type safety is a good thing and relying on byte concat order in Java APIs to delimit data is a horrible thing :)
So, if I get you correctly, the Android TEE
AES/GCM/NoPadding
Cipher puts the tag at the start of the output? -
reporter Thanks for the link!
So, if I get you correctly, the Android TEE
AES/GCM/NoPadding
Cipher puts the tag at the start of the output?To be honest I am not sure yet. Do you have any suggestions what is the best way to find this out? Directly Comparing Java and Android TEE outputs with the same inputs?
-
reporter Seems the
Caused by: android.security.KeyStoreException: Incompatible block mode
will also come when I do not use Nimbus. From something like this:
val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) keyStore.load(null, null) val secretKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.SecretKeyEntry val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, secretKeyEntry.secretKey) return cipher.doFinal(message.toByteArray(Charset.forName("UTF-8")))
The error itself is generated from https://android.googlesource.com/platform/system/keymaster/+/android-m-preview/aes_operation.cpp#79
I am using some invalid parameter here I think. Probably during key import as there GCM is not specified. With “AES/CBC/PKCS7PADDING“ same thing above works just fine
Small follow-up on the IV part - seems by default Android does not allow you to set it. Official feature.
But this can be disabled if wanted via https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setRandomizedEncryptionRequired(boolean)
-
reporter Seems I am on the correct track here. If in the key import code I comment out this part here: https://github.com/LabMobi/HardwareKeyImportDemoAndroid/blob/feature/1-use-aesgcm-for-the-jwe-encrypted-message/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt#L61
then “AES/CBC/PKCS7PADDING“ also starts throwing “
Incompatible block mode
“ -
reporter Ok, it seems to me I need to add both the GCM block mode and the
Tag::MIN_MAC_LENGTH
. I’ll have to investigate how to do the latter.Anyways, there is a strong indication that the “
android.security.KeyStoreException: Incompatible block mode
“ is my mistake and the result on the attributes set in the import process.When I now try with:
-
TEE-backed key generated locally with mode GCM and NoPadding padding
- Bypasses the import GCM mode issue described above
-
and No-IV requirement disabled with
setRandomizedEncryptionRequired
- Bypasses the Nimbus-provided IV issue described above
-
and using Nimbus
Then encryption to JWE works fine.
-
- Log in to comment
Would you post the stack trace you get when the code tries to get the key length?
I was thinking to skip the key length check in case the underlying key store cannot and doesn't want to report it, but keep it otherwise for in-memory keys, because it prevents common developer errors.
Once those checks are smartened so they don't get into the way, I suppose the current AES/GCM cipher will work just as it is.
The current code for the AES/CBC ciphers (the one you were referring to) will likely need a big rework.
Would you be able to get a list of the supported ciphers in your Android / TEE env? I'm not an Android developer, but something like that should do: https://connect2id.com/products/nimbus-jose-jwt/examples/pkcs11#list-algs