Search code examples
androidprotocol-buffersandroid-keystoreandroid-jetpack-datastore

AndroidX DataStore - AES/CBC/PKCS7 - javax.crypto.IllegalBlockSizeException


I read Mark Allison's blog post about combining the new Android DataStore with encryption with the use of the Android Keystore.

I'm using the same exact SecretKey properties (AES/CBC/PKCS7) and Encrypt/Decrypt found in his blog.

class AesCipherProvider(
    private val keyName: String,
    private val keyStore: KeyStore,
    private val keyStoreName: String
) : CipherProvider {

    override val encryptCipher: Cipher
        get() = Cipher.getInstance(TRANSFORMATION).apply {
            init(Cipher.ENCRYPT_MODE, getOrCreateKey())
        }

    override fun decryptCipher(iv: ByteArray): Cipher =
        Cipher.getInstance(TRANSFORMATION).apply {
            init(Cipher.DECRYPT_MODE, getOrCreateKey(), IvParameterSpec(iv))
        }

    private fun getOrCreateKey(): SecretKey =
        (keyStore.getEntry(keyName, null) as? KeyStore.SecretKeyEntry)?.secretKey
            ?: generateKey()

    private fun generateKey(): SecretKey =
        KeyGenerator.getInstance(ALGORITHM, keyStoreName)
            .apply { init(keyGenParams) }
            .generateKey()

    private val keyGenParams =
        KeyGenParameterSpec.Builder(
            keyName,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        ).apply {
            setBlockModes(BLOCK_MODE)
            setEncryptionPaddings(PADDING)
            setUserAuthenticationRequired(false)
            setRandomizedEncryptionRequired(true)
        }.build()

    private companion object {
        const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
        const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
        const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
        const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
    }
}
class CryptoImpl constructor(private val cipherProvider: CipherProvider) : Crypto {

    override fun encrypt(rawBytes: ByteArray, outputStream: OutputStream) {
        val cipher = cipherProvider.encryptCipher
        val encryptedBytes = cipher.doFinal(rawBytes)
        with(outputStream) {
            write(cipher.iv.size)
            write(cipher.iv)
            write(encryptedBytes.size)
            write(encryptedBytes)
        }
    }

    override fun decrypt(inputStream: InputStream): ByteArray {
        val ivSize = inputStream.read()
        val iv = ByteArray(ivSize)
        inputStream.read(iv)
        val encryptedDataSize = inputStream.read()
        val encryptedData = ByteArray(encryptedDataSize)
        inputStream.read(encryptedData)
        val cipher = cipherProvider.decryptCipher(iv)
        return cipher.doFinal(encryptedData)
    }
}

I'm using following super simple ProtocolBuffer with only one String field.

syntax = "proto3";

option java_package = "my.package.model";

message SimpleData {
    string text = 1;
}

I'm using following code to test this implementation.

class SecureSimpleDataSerializer(private val crypto: Crypto) :
    Serializer<SimpleData> {

    override fun readFrom(input: InputStream): SimpleData {
        return if (input.available() != 0) {
            try {
                SimpleData.ADAPTER.decode(crypto.decrypt(input))
            } catch (exception: IOException) {
                throw CorruptionException("Cannot read proto", exception)
            }
        } else {
            SimpleData("")
        }
    }

    override fun writeTo(t: SimpleData, output: OutputStream) {
        crypto.encrypt(SimpleData.ADAPTER.encode(t), output)
    }

    override val defaultValue: SimpleData = SimpleData()
}

private val simpleDataStore = createDataStore(
    fileName = "SimpleDataStoreTest.pb",
    serializer = SecureSimpleDataSerializer(
        CryptoImpl(
            AesCipherProvider(
                "SimpleDataKey",
                KeyStore.getInstance("AndroidKeyStore").apply { load(null) },
                "AndroidKeyStore"
            )
        )
    )
)

When I try to serialize and deserialize a simple String it works like intended.

simpleDataStore.updateData { it.copy(text = "simple-string") }
println(simpleDataStore.data.first())
// "simple-string"

However when I try the same with a longer String (note smaller than the max size for Proto's).
The save works, but upon killing the app and relaunching the app to retrieve the value it crashes.

simpleDataStore.updateData { it.copy(text = "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQeyJhdWQiOiJ2cnRudS1zaXRlIiwic3ViIjoiNmRlNjg1MjctNGVjMi00MmUwLTg0YmEtNGU5ZjE3ZTQ4MmY2IiwiaXNzIjoiaHR0cHM6XC9cL2xvZ2luLnZydC5iZSIsInNjb3BlcyI6ImFkZHJlc3Msb3BlbmlkLHByb2ZpbGUsbGVnYWN5aWQsbWlkLGVtYWlsIiwiZXhwIjoxNjEwMjc4OTQ0LCJpYXQiOjE2MTAyNzUzNDQsImp0aSI6Ijc0MDk3MzFiLTg5OGUtNGVmNS1iNWMwLTEzODM2ZWZjN2ZjOCJ9kSkuI9Z0XLLBtfC0SpHA4wV0299ZOd6Xj99hNkemim7fRP1ooCD8YkqbM0hhBKiiYbvhqmfc1NSKYHAehA7Z9c6XluPTIpZkljHIBH7BLd0IGznraUEOMYDh0I2aQKZxxvwV6RlWetdCBUf3KtQuDO7snywbE5jmhzq75Y") }
println(simpleDataStore.data.first())
Process: com.stylingandroid.datastore, PID: 13706
    javax.crypto.IllegalBlockSizeException
        at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:513)
        at javax.crypto.Cipher.doFinal(Cipher.java:2055)
        at com.stylingandroid.datastore.security.CryptoImpl.decrypt(Crypto.kt:33)
        at com.stylingandroid.datastore.ui.MainActivity$SecureSimpleDataSerializer.readFrom(MainActivity.kt:32)
        at com.stylingandroid.datastore.ui.MainActivity$SecureSimpleDataSerializer.readFrom(MainActivity.kt:26)
        at androidx.datastore.core.SingleProcessDataStore.readData(SingleProcessDataStore.kt:249)
        at androidx.datastore.core.SingleProcessDataStore.readDataOrHandleCorruption(SingleProcessDataStore.kt:227)
        at androidx.datastore.core.SingleProcessDataStore.readAndInitOnce(SingleProcessDataStore.kt:190)
        at androidx.datastore.core.SingleProcessDataStore$actor$1.invokeSuspend(SingleProcessDataStore.kt:154)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
     Caused by: android.security.KeyStoreException: Invalid input length
        at android.security.KeyStore.getKeyStoreException(KeyStore.java:1301)
        at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:176)
        at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:506)
        at javax.crypto.Cipher.doFinal(Cipher.java:2055) 
        at com.stylingandroid.datastore.security.CryptoImpl.decrypt(Crypto.kt:33) 
        at com.stylingandroid.datastore.ui.MainActivity$SecureSimpleDataSerializer.readFrom(MainActivity.kt:32) 
        at com.stylingandroid.datastore.ui.MainActivity$SecureSimpleDataSerializer.readFrom(MainActivity.kt:26) 
        at androidx.datastore.core.SingleProcessDataStore.readData(SingleProcessDataStore.kt:249) 
        at androidx.datastore.core.SingleProcessDataStore.readDataOrHandleCorruption(SingleProcessDataStore.kt:227) 
        at androidx.datastore.core.SingleProcessDataStore.readAndInitOnce(SingleProcessDataStore.kt:190) 
        at androidx.datastore.core.SingleProcessDataStore$actor$1.invokeSuspend(SingleProcessDataStore.kt:154) 
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) 
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) 
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571) 
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738) 
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678) 
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665) 
2021-01-10 14:08:09.907 13706-13706/com.stylingandroid.datastore I/Process: Sending signal. PID: 13706 SIG: 9

Does anybody know?
Is it specific to the String's length in combination with the chosen encryption algorithm?
Is the decryption function wrong?

Thanks in advance.


Solution

  • The problem is reproducible on my machine. It occurs when the encrypted data encryptedBytes in CryptoImpl.encrypt has a length of more than 255 bytes. The reason is that starting with 256 bytes encryptedBytes.size cannot be stored on one byte, while the methods int InputStream.read() or void OutputStream.write(int) read or write only one byte.

    Therefore, if the size of the ciphertext is to be written, a sufficiently large bytes buffer must be used in CryptoImpl.encrypt, e.g. 4 bytes:

    with(outputStream) {
        write(cipher.iv.size)
        write(cipher.iv)
        write(ByteBuffer.allocate(4).putInt(encryptedBytes.size).array())   // Convert Int to 4 bytes buffer
        write(encryptedBytes)
    }
    

    and for reading in CryptoImpl.decrypt:

    val ivSize = inputStream.read()
    val iv = ByteArray(ivSize)
    inputStream.read(iv)
    
    val encryptedDataSizeBytes = ByteArray(4)
    inputStream.read(encryptedDataSizeBytes)
    val encryptedDataSize = ByteBuffer.wrap(encryptedDataSizeBytes).int     // Convert 4 bytes buffer to Int
    val encryptedData = ByteArray(encryptedDataSize)
    inputStream.read(encryptedData)
    

    However, writing the sizes is actually not necessary. The size of the IV is known, it corresponds to the block size, i.e. 16 bytes for AES, so that the criterion for the separation of IV and ciphertext is defined. Thus, the data can be written in CryptoImpl.encrypt as follows:

    with(outputStream) {
        write(cipher.iv)                         // Write 16 bytes IV 
        write(encryptedBytes)                    // Write ciphertext
    }
    

    And for reading in CryptoImpl.decrypt:

    val iv = ByteArray(16)
    inputStream.read(iv)                         // Read IV (first 16 bytes) 
    val encryptedData = inputStream.readBytes()  // Read ciphertext (remaining data)