JWE with shared key support for Android Hardware KeyStore (TEE) keys

Issue #490 open
Harri Kirik created an issue

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:

  1. 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.

    1. See https://bitbucket.org/connect2id/nimbus-jose-jwt/src/d8a4358e56328637974b343a0834da67e20a6e30/src/main/java/com/nimbusds/jose/crypto/impl/DirectCryptoProvider.java#lines-124
  2. 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.

  3. 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.
  4. 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)

  1. Vladimir Dzhuvinov
    • changed status to open

    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

  2. Harri Kirik 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.

  3. Vladimir Dzhuvinov

    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.

  4. Harri Kirik 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:

    1. 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
    2. 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
    3. 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
    4. 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

  5. Harri Kirik reporter

    Small update to the demo code:

    1. The Demo at https://github.com/LabMobi/HardwareKeyImportDemoAndroid now tries both communication directions:

      1. Server software key encryption → client TEE key decryption. This has the problem described above
      2. Client TEE key encryption → server software key encryption. This also fails on the TEE part, see below
    2. 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!

  6. Vladimir Dzhuvinov

    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 :) )

  7. Harri Kirik 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#SupportedCiphers

    I 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.

  8. Harri Kirik 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

  9. Harri Kirik 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
    

  10. Harri Kirik 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“:

  11. Vladimir Dzhuvinov

    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

  12. Harri Kirik 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.

  13. Harri Kirik 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

  14. Vladimir Dzhuvinov

    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 the A128GCM 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

  15. Harri Kirik 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.

    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);
    

    See https://android.googlesource.com/platform/cts/+/master/tests/tests/keystore/src/android/keystore/cts/ImportWrappedKeyTest.java#353

    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 ..

  16. Harri Kirik 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?

  17. Harri Kirik 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)

  18. Harri Kirik 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:

    1. TEE-backed key generated locally with mode GCM and NoPadding padding

      1. Bypasses the import GCM mode issue described above
    2. and No-IV requirement disabled with setRandomizedEncryptionRequired

      1. Bypasses the Nimbus-provided IV issue described above
    3. and using Nimbus

    Then encryption to JWE works fine.

  19. Log in to comment