Java 特定Android设备/操作系统上的增量加密问题

Java 特定Android设备/操作系统上的增量加密问题,java,android,kotlin,encryption,cryptography,Java,Android,Kotlin,Encryption,Cryptography,我正在使用增量加密,并结合Android密钥库提供程序 val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) val chunks = textToEncrypt.chunked(CHUNK_SIZE) val encryptedChunks: MutableList<ByteArray?> = mutableListOf() chunks.

我正在使用增量加密,并结合Android密钥库提供程序

val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())

val chunks = textToEncrypt.chunked(CHUNK_SIZE)
val encryptedChunks: MutableList<ByteArray?> = mutableListOf()

chunks.forEachIndexed { index, chunk ->
     if (index == chunks.size - 1) {
            encryptedChunks.add(cipher.doFinal(chunk.toByteArray(StandardCharsets.UTF_8)))
     } else {
            encryptedChunks.add(cipher.update(chunk.toByteArray(StandardCharsets.UTF_8)))
     }
}

val result = encryptedChunks.filterNotNull().reduce { acc, item -> acc.plus(item) }
现在,这段代码已经在30多种不同的设备上进行了大量测试,除了一部手机(Xperia XA with Android 7.0)之外,从未出现过任何问题。对于这款手机,如果输入(
textToEncrypt
)足够小,可以在单个数据块中对所有内容进行加密,那么就可以了,但是如果输入的数据块更大(通常在100KiB左右),需要更多的数据块,那么就无法对数据进行加密。这就是我得到的:

Caused by javax.crypto.IllegalBlockSizeException
   at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:491)
   at javax.crypto.Cipher.doFinal(Cipher.java:2056)

Caused by android.security.KeyStoreException: Memory allocation failed
   at android.security.KeyStore.getKeyStoreException(KeyStore.java:685)
   at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.update(KeyStoreCryptoOperationChunkedStreamer.java:132)
   at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineUpdate(AndroidKeyStoreCipherSpiBase.java:338)
   at javax.crypto.Cipher.update(Cipher.java:1683)
注意:仅此设备的
cipher.update()
加密模式下返回null,这就是为什么在我的代码中我允许返回null,然后丢弃它们以形成加密数据。
这意味着
cipher.doFinal
应该一次性返回整个加密数据


编辑:显然,只有这款手机的数据块大小不合适:它不能是32Kb,但8Kb可以正常工作

我花了一些时间分析这个问题。我找不到真正的原因,但至少我找到了一些元素,它们就在这里

多部件加工 首先,您说您使用了32Kb的块大小,但事实并非如此。将字符串拆分为32Kc的块(32768个字符),然后将每个块转换为字节数组。由于字符的UTF-8表示形式可以从1到4字节不等,因此字节数组通常大于32Kb(除非只有ASCII字符)

您应该首先将字符串转换为字节数组,然后将其拆分为32Kb的块。只有这样才能保证传递给crypto API的缓冲区的大小

客户端错误 现在,关于你得到的stacktrace。与乍一看的情况相反,错误不会出现在
doFinal()
中,而是出现在
update()
中。当您调用
update()
时,调用将委派给。有趣的是:

try {
    flushAAD();
    output = mMainDataStreamer.update(input, inputOffset, inputLen);
} catch (KeyStoreException e) {
    mCachedException = e;
    return null;
}
它调用
mMainDataStreamer.update()
,该操作失败并抛出带有代码的
KeyStoreException
。但是异常被捕获,存储在
mCachedException
中,并返回
null
。这就是为什么调用
update()
时会得到
null

调用
doFinal()
时,它调用:

该方法发现有一个缓存的异常并抛出它(包装在一个
IllegalBlockSizeException
中,它与实际问题完全无关)

密钥库错误 现在,真正的问题。加密/解密的实际工作是由密钥存储服务执行的,这是一个用C++编写的单独进程。AES的相关部分在中

该文件中返回了许多
KM\u错误\u内存\u分配\u失败的
错误。顾名思义,该代码意味着内存分配失败。因此,出于某种原因,密钥库似乎无法分配缓冲区。很难理解为什么

结论
由于真正的原因是神秘的,我建议保持一个小的缓冲区大小,并按照开头所述更改拆分过程。

继续讨论Olivier的答案。他已确定异常是由
mMainDataStreamer.update()
引发的。如果您查看类,您将看到
mMainDataStreamer
是类的一个实例。这里有一个有趣的部分:

// Binder buffer is about 1MB, but it's shared between all active transactions of the process.
// Thus, it's safer to use a much smaller upper bound.
private static final int DEFAULT_MAX_CHUNK_SIZE = 64 * 1024;
在我们的例子中,使用默认的最大块大小<代码>默认\u最大\u块大小
对块大小设置上限。如果将较大的块传递给
cipher.update()
方法,它们将被切割成
DEFAULT\u MAX\u CHUNK\u SIZE
的块。正如你所看到的,即使是Android的开发者也没有新的精确的安全块大小,他们不得不自己猜测(在你的例子中,没有成功)

但是,请注意,缓冲区用于将这些块传递到加密过程并从中返回结果。它的大小只有1MB左右

也许在这个特殊的设备上有一个非常小的活页夹缓冲区?你可以试着用这个答案来研究它:

在未来的设备上,您可以使用:

IBinder.getSuggestedMaxIpcSizeBytes()

您是否尝试过增加块大小?嗨@Mike,不,我没有,但我想先尝试一种确定性方法。比如发现真正的原因,然后应用修复。因为是的,我可以尝试所有不同的块大小,但我永远无法保证它能在每个设备上工作。它说内存分配失败,所以自然要做的是减小块大小。试着把它减少到32。“真正的原因”可能只是一个有缺陷的设备,甚至是一个有缺陷的模型。是的,它可能是一个有缺陷的设备。当前块已经很小了:32Kb。我不确定买一个小一点的有什么意义。我只是不想扔掉我们到目前为止所做的所有测试,改变块大小。@JohannesB所以结果是肯定的,对于特定的手机,使用特定的版本(7.0),块太大了。真不敢相信!非常感谢您的调查!:)块的大小与输入的大小完全相同,即使存在转换,因为输入只包含ASCII。但即使不是,我的32Kb也是任意的,因为找不到
update()
方法支持的最大值或至少是推荐的值。
// Binder buffer is about 1MB, but it's shared between all active transactions of the process.
// Thus, it's safer to use a much smaller upper bound.
private static final int DEFAULT_MAX_CHUNK_SIZE = 64 * 1024;
IBinder.getSuggestedMaxIpcSizeBytes()