1

我阅读了 Mark Allison 的博客文章,其中介绍了将新的Android DataStore与加密与 Android Keystore 的使用相结合。

我正在使用在他的博客中找到的完全相同的 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

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

提前致谢。

4

1 回答 1

1

这个问题可以在我的机器上重现。当加密数据encryptedBytesCryptoImpl.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)
于 2021-01-11T10:28:14.650 回答