AndroidX 数据存储 - AES/CBC/PKCS7 - javax.crypto.IllegalBlockSizeException

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

我阅读了 Mark Allison 的 blog post about combining the new Android DataStore,其中使用 Android 密钥库进行了加密。

我使用的是在他的博客中找到的完全相同的 SecretKey 属性 (AES/CBC/PKCS7) 和 Encrypt/Decrypt。

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

我正在使用以下超级简单的 ProtocolBuffer,只有一个 String 字段。

syntax = "proto3";

option java_package = "my.package.model";

message SimpleData {
    string text = 1;
}

我正在使用以下代码来测试此实现。

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

当我尝试序列化和反序列化一个简单的 String 时,它按预期工作。

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

然而,当我尝试使用更长的 String 时(注意小于 Proto 的最大尺寸)。
保存有效,但在终止应用程序并重新启动应用程序以检索它崩溃的值时。

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

有人知道吗?
是否特定于字符串的长度以及所选的加密算法?
是不是解密函数出错了?

提前致谢。

问题在我的机器上重现。当CryptoImpl.encrypt中的加密数据encryptedBytes长度超过255字节时,就会出现这种情况。原因是从256字节开始encryptedBytes.size不能存储在一个字节上,而方法int InputStream.read()void OutputStream.write(int)只读取或写入一个字节

因此,如果要写入密文的大小,CryptoImpl.encrypt中必须使用足够大的字节缓冲区,例如4 个字节:

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

并在 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)

然而,实际上没有必要写尺寸。 IV 的大小是已知的,它对应于块大小,即 AES 为 16 字节,因此定义了 IV 和密文分离的标准。这样,CryptoImpl.encrypt中的数据可以这样写:

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

阅读CryptoImpl.decrypt

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