微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决AndroidX 数据存储 - AES/CBC/PKCS7 - javax.crypto.IllegalBlockSizeException

我阅读了 Mark Allison 的 blog post 关于将新的 Android DataStore 与加密与 Android 密钥库的使用相结合。

我正在使用在他的博客中找到的完全相同的 SecretKey 属性 (AES/CBC/PKCS7) 和加密/解密。

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

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

提前致谢。

解决方法

该问题可在我的机器上重现。当 encryptedBytes 中的加密数据 CryptoImpl.encrypt 的长度超过 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)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。