0

I am trying to implement biometric authentication for encryption and decryption of a JWToken. The token is received and encrypted after user authenticated at the server. The encryption of the token should be permitted without biometric user authorization at the device. Only decryption operations should require biometric device authorization. This is why I use an asymmetric key for this. The token I receive however is around 1000 bytes and hence cannot be encrypted with asymmetric key. This is why I create a symmetric key for decryption and encryption operations and wrap and unwrap this key with the authorization protected asymmetric key. However, trying to wrap the symmetric key with the asymmetric public key throws InvalidKeyException with message: javax.crypto.BadPaddingException. Trying to wrap the symmetric key with the asymmetric private key also throws InvalidKeyException this time with the brief message: Failed to unwrap key. When i setUserAuthenticationRequired to false the asymmetric private key works just fine for wrapping. What am I doing wrong here?

class EncryptionService(
    private val wrappedKeyStoreSource: WrappedKeyStore2
) {

    companion object {
        const val MASTER_KEY = "master_key"
        const val ALGORITHM_AES = "AES"
        const val ENCRYPTION_KEY = "encryption_key"
        const val TRANSFORMATION_ASYMMETRIC = "RSA/ECB/PKCS1Padding"
        const val TRANSFORMATION_SYMMETRIC = "AES/CBC/PKCS7Padding"
    }

    private val keyStore: KeyStore = createAndroidKeyStore()

    init {
        createDefaultSymmetricKey()
    }

    fun encrypt(data: String): String =
        encryptWithSymmetricKey(data)

    private fun createDefaultSymmetricKey() {
        val symmetricKey = generateSymmetricKey()
        val masterKey = createAsymmetricKey(MASTER_KEY)
        val cipher: Cipher = Cipher.getInstance(TRANSFORMATION_ASYMMETRIC)
        cipher.init(Cipher.WRAP_MODE, masterKey.public)
        val encryptedSymmetricKey = cipher.wrap(symmetricKey)
        wrappedKeyStoreSource.saveKey(ENCRYPTION_KEY, Base64.encodeToString(encryptedSymmetricKey, Base64.DEFAULT) )
    }

    //encrypt without user authorization
    private fun encryptWithSymmetricKey(data: String): String {
        val masterKey = getAsymmetricKeyPair(MASTER_KEY)
        val encryptionKey = wrappedKeyStoreSource.getKey(ENCRYPTION_KEY)
        val unwrapCipher: Cipher = Cipher.getInstance(TRANSFORMATION_ASYMMETRIC)
        unwrapCipher.init(Cipher.UNWRAP_MODE, masterKey?.public)
        val encryptedKeyData = Base64.decode(encryptionKey, Base64.DEFAULT)

       //this line throws InvalidKeyException 
        //unwrap with public key throws InvalidKeyException with message: javax.crypto.BadPaddingException.
        //unwrap with private key throws InvalidKeyException if setUserAuthenticationRequired on this
        // key is set to true with message: Failed to unwrap key (maybe due to UserNotAuthenticatedException?)
        val symmetricKey = unwrapCipher.unwrap(encryptedKeyData, ALGORITHM_AES, Cipher.PUBLIC_KEY) as SecretKey

        val encryptCipher: Cipher = Cipher.getInstance(TRANSFORMATION_SYMMETRIC)
        encryptCipher.init(Cipher.ENCRYPT_MODE, symmetricKey)
        val encryptedString = encryptCipher.doFinal(data.toByteArray())
        return Base64.encodeToString(encryptedString, Base64.DEFAULT)
    }

    private fun createAsymmetricKey(alias: String): KeyPair {
        val generator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore")
        val builder = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
            .setUserAuthenticationRequired(true)
        generator.initialize(builder.build())
        return generator.generateKeyPair()
    }

    private fun generateSymmetricKey(): SecretKey {
        val keyGenerator = KeyGenerator.getInstance("AES")
        return keyGenerator.generateKey()
    }

    private fun getAsymmetricKeyPair(alias: String): KeyPair? {
        val privateKey = keyStore.getKey(alias, null) as PrivateKey?
        val publicKey = keyStore.getCertificate(alias)?.publicKey
        return if (privateKey != null && publicKey != null) {
            KeyPair(publicKey, privateKey)
        } else {
            null
        }
    }

    private fun createAndroidKeyStore(): KeyStore {
        val keyStore = KeyStore.getInstance("AndroidKeyStore")
        keyStore.load(null)
        return keyStore
    }

}
stackTrace: java.security.InvalidKeyException: javax.crypto.BadPaddingException: 
error:0400006b:RSA routines:OPENSSL_internal:BLOCK_TYPE_IS_NOT_01
        at com.android.org.conscrypt.OpenSSLCipherRSA.engineUnwrap(OpenSSLCipherRSA.java:373)
        at javax.crypto.Cipher.unwrap(Cipher.java:2440)
        at com.libencryption.data.EncryptionService.encryptWithSymmetricKey(EncryptionService.kt:58)
        at com.libencryption.data.EncryptionService.encrypt(EncryptionService.kt:37)

when I switch to unwrap with private key as suggested below like so

unwrapCipher.init(Cipher.UNWRAP_MODE, masterKey?.private)
val encryptedKeyData = Base64.decode(encryptionKey, Base64.DEFAULT)
val symmetricKey = unwrapCipher.unwrap(encryptedKeyData, ALGORITHM_AES, Cipher.SECRET_KEY) as SecretKey

i get the following stacktrace

stackTrace: java.security.InvalidKeyException: Failed to unwrap key
        at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineUnwrap(AndroidKeyStoreCipherSpiBase.java:682)
        at javax.crypto.Cipher.unwrap(Cipher.java:2440)
        at com.libencryption.data.EncryptionService.encryptWithSymmetricKey(EncryptionService.kt:58)
        at com.libencryption.data.EncryptionService.encrypt(EncryptionService.kt:37)

which I assume is caused because i setUserAuthenticationRequired to true since when I set it to false everything encrypts just fine. However as you can see from the stackstrace that exception is not logged. Is there any other reason why that fails like that with setUserAuthenticationRequired to true?

Macs
  • 197
  • 2
  • 15
  • 1
    In Public/Private key encryption the **encryption** is done with the **PublicKey** and the **decryption** in performed with the **PrivateKey**. – Michael Fehr Oct 09 '20 at 21:15
  • Don't attempt to paraphrase what an exception says, instead post the entire exception stacktrace **and** tell us what line in your code triggered the exception. Even better, also include the actual keys involved. The goal is to have a complete example that illustrates the problem. – President James K. Polk Oct 09 '20 at 22:50
  • ok. my mistake. is there any alternative option then? except for: - user authorizes each encryption with symmetric key - manually separating data and encrypt its parts with public asymmetric key – Macs Oct 09 '20 at 22:56
  • @PresidentJamesK.Polk. I added the stacktrace and put a comment where the exception is thrown. not quite sure what you mean by include the actual keys involved – Macs Oct 09 '20 at 23:36
  • Again my point, sorry. In your function "encryptWithSymmetricKey" you are trying to unwrap the (with RSA encrypted) symmetric key with this code line: "unwrapCipher.init(Cipher.UNWRAP_MODE, masterKey?.public)". You are trying to unwrap = decrypt with the **PUBLIC** key - try to change to **PRIVATE**. – Michael Fehr Oct 10 '20 at 07:47
  • Since you are wrapping/unwrapping a symmetric key, the third parameter in [`unwrapCipher.unwrap()`](https://developer.android.com/reference/javax/crypto/Cipher#unwrap(byte[],%20java.lang.String,%20int)) should be `SECRET_KEY` (in addition to decrypting/unwrapping with the private key, see other comments). – Topaco Oct 10 '20 at 07:59
  • I totally agree with all of you that the public key is designed for encryption and that I should not use it the way I did. But remember using the private key requires user authorization for decryption in my setup. Which is why I used asymmetric keys in the first place. But maybe considering the size of data I need to encrypt and the key limits of this procure maybe I should use symmetric encryption. which of course comes at the cost of user having to fingerprint authorize after registration and server authorization (were the JWToken is encrypted) – Macs Oct 10 '20 at 09:00
  • that the whole wrapping and exporting exercise of the public key (public key is never shared with server though) is only needed because of the asymmetric key limits. – Macs Oct 10 '20 at 09:08
  • I just realized that biometric authentication in combination with server authentication and JWToken encryption with symmetric keys (which means a user has to fingerprint authenticate for each JWToken encryption operation) is not doable because each time the toke expires (which can be quite shored lived) use has to fingerprint authenticate to store the (usually behind the scenes) refreshed token – Macs Oct 10 '20 at 10:08
  • What I meant is that with a biometrics base symmetric encryption one can not directly provide the cipher for encrypting/decrypting a JWToken to androids biometrics prompt. We need to wrap/unwrap a encryption key that has no authorization and size requirements by passing that to the biometrics prompt instead. Once user authenticated at the server (to receive the token) and authenticated by fingerprint (to receive the token encryption key). all token refresh operations (which include decryption and encryption) are handled by the then unwrapped symmetric key – Macs Oct 10 '20 at 10:43

0 Answers0