在特定的安卓设备/操作系统上增量加密存在问题。

huangapple go评论96阅读模式
英文:

Issue with incremental encrpyption on specific Android device/OS

问题

我正在使用增量加密,与Android KeyStore提供程序结合使用。

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

这些是我正在使用的常量:

const val TRANSFORMATION = "AES/GCM/NoPadding"
const val CHUNK_SIZE = 32768 // 32KiB

现在,这段代码在超过30部不同设备上进行了大量测试,除了一部手机(搭载Android 7.0的Xperia XA)之外,从未出现任何问题。对于这部手机,如果输入(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()ENCRYPT_MODE 下返回 null,这就是为什么在我的代码中我允许返回 null,然后将其舍弃以形成加密数据。
这意味着 cipher.doFinal 应该一次性返回整个加密数据。

编辑:所以显然只有对于这部手机,块大小不合适:不能是32Kb,但8Kb 的大小可以正常工作。

英文:

I am using incremental encrpyption, in combination with Android KeyStore provider.

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

val chunks = textToEncrypt.chunked(CHUNK_SIZE)
val encryptedChunks: MutableList&lt;ByteArray?&gt; = mutableListOf()

chunks.forEachIndexed { index, chunk -&gt;
     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 -&gt; acc.plus(item) }

These are the constants I am using:

const val TRANSFORMATION = &quot;AES/GCM/NoPadding&quot;
const val CHUNK_SIZE = 32768 // 32KiB

Now, this code has been heavily tested on more than 30 different devices, and never had any problem whatsoever, except with one phone (Xperia XA with Android 7.0). For this phone, if the input (textToEncrypt) is small enough that everything can be encrypted in a single chunk then it is fine, but if it is bigger (normally around 100KiB), so that it needs more chunks, then it will not be able to encrypt the data. This is what I get:

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)

NOTE: Only for this device the cipher.update() returns null with ENCRYPT_MODE , that's why in my code I allow to return null, and then discard them for forming the encrypted data.
This means that cipher.doFinal is supposed to return in one go the whole encrypted data.

EDIT: So apparently only for this phone the chunk size is not fine: it cannot be 32Kb, but 8Kb works fine

答案1

得分: 5

在Olivier的回答基础上进行扩展。他确定异常是由mMainDataStreamer.update()引发的。如果你查看AndroidKeyStoreCipherSpiBase 类,你会发现mMainDataStreamerKeyStoreCryptoOperationChunkedStreamer类的一个实例。以下是一个有趣的部分:

// Binder缓冲区约为1MB,但它在进程的所有活动事务之间共享。
// 因此,更安全的做法是使用一个较小的上限。
private static final int DEFAULT_MAX_CHUNK_SIZE = 64 * 1024;

在我们的情况下,使用了这个默认的最大块大小。DEFAULT_MAX_CHUNK_SIZE对块大小设置了上限。如果你将更大的块传递给cipher.update()方法,它们将被分割成大小为DEFAULT_MAX_CHUNK_SIZE的块。正如你所见,即使是Android的开发人员也不知道确切的安全块大小,不得不自己猜测(在你的情况下是不成功的)。

然而,注意到Binder缓冲区用于将这些块传递给加密过程,并从中获取结果。它的大小只有约1MB。

也许在这个特定设备上有一个异常小的Binder缓冲区?你可以尝试通过使用这个答案来查看:https://stackoverflow.com/a/25666064/3249257

在将来的设备上,你可以使用:

IBinder.getSuggestedMaxIpcSizeBytes()

https://developer.android.com/reference/android/os/IBinder#getSuggestedMaxIpcSizeBytes/

英文:

Expanding on Olivier's answer. He's determined that the exception is thrown by mMainDataStreamer.update(). If you look at AndroidKeyStoreCipherSpiBase class you will see that mMainDataStreamer is an instance of KeyStoreCryptoOperationChunkedStreamer class. Here's an interesting part:

// Binder buffer is about 1MB, but it&#39;s shared between all active transactions of the process.
// Thus, it&#39;s safer to use a much smaller upper bound.
private static final int DEFAULT_MAX_CHUNK_SIZE = 64 * 1024;

In our case, this default max chunk size is used. DEFAULT_MAX_CHUNK_SIZE puts an upper limit on the chunk size. If you pass bigger chunks to the cipher.update() method they will be cut into chunks of DEFAULT_MAX_CHUNK_SIZE. As you can see, even developers of Android didn't new the precise safe chunk size and had to guess themselves (unsuccessfully, in your case).

However, notice that Binder buffer is used to pass these chunks to the encryption process and results back from it. And it's size is only about 1MB.

Maybe there is an unusually small Binder buffer on this particular device? You can try to look into it by using that answer: https://stackoverflow.com/a/25666064/3249257

On future devices, you can use:

IBinder.getSuggestedMaxIpcSizeBytes()

https://developer.android.com/reference/android/os/IBinder#getSuggestedMaxIpcSizeBytes()

答案2

得分: 2

我花了一些时间来分析这个问题。我找不到真正的原因,但至少我找到了一些元素,所以在这里列出来。

多部分处理

首先,你说你使用了32Kb的块大小,但事实并非如此。你将字符串分割成32Kc(32768个字符)大小的块,然后将每个块转换为字节数组。由于一个字符的UTF-8表示可以是1到4个字节,你的字节数组通常会比32Kb大(除非只有ASCII字符)。

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

客户端错误

现在,关于你得到的堆栈跟踪。与一开始看起来的不同,错误不是发生在doFinal()而是在update()中。当你调用update()时,调用会委托给AndroidKeyStoreCipherSpiBase.engineUpdate()。有趣的部分是:

try {
    flushAAD();
    output = mMainDataStreamer.update(input, inputOffset, inputLen);
} catch (KeyStoreException e) {
    mCachedException = e;
    return null;
}

它调用了mMainDataStreamer.update(),它失败并抛出一个带有代码 KM_ERROR_MEMORY_ALLOCATION_FAILEDKeyStoreException。但异常被捕获,存储在mCachedException中,然后返回null。这就是你调用update()时得到null的原因。

当你调用doFinal()时,它会调用AndroidKeyStoreCipherSpiBase.engineDoFinal()

protected final byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen)
        throws IllegalBlockSizeException, BadPaddingException {
    if (mCachedException != null) {
        throw (IllegalBlockSizeException)
            new IllegalBlockSizeException().initCause(mCachedException);
    }

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

密钥库错误

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

在该文件中返回了许多KM_ERROR_MEMORY_ALLOCATION_FAILED错误。顾名思义,这个代码表示内存分配失败。因此,似乎由于某种原因,密钥库无法分配缓冲区。很难理解为什么会出现这种情况。

结论

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

英文:

I've spent some time analyzing the issue. I couldn't find the real cause, but at least I have found some elements, so here they are.

Multi-part processing

First, you say that you use a chunk size of 32Kb, but it's not really true. You split the string in chunks of 32Kc (32768 characters), then you convert each chunk to a byte array. Since the UTF-8 representation of a char can range from 1 to 4 bytes, your byte array will generally be larger than 32Kb (unless you have only ASCII chars).

You should first convert the string to a byte array, then split it into 32Kb chunks. Only that will guarantee the size of the buffer that you pass to the crypto API.

Client-side error

Now, about the stacktrace you get. Contrary to what it seems at first glance, the error doesn't occur in doFinal() but in update(). When you call update(), the call is delegated to AndroidKeyStoreCipherSpiBase.engineUpdate(). The interesting part is:

try {
    flushAAD();
    output = mMainDataStreamer.update(input, inputOffset, inputLen);
} catch (KeyStoreException e) {
    mCachedException = e;
    return null;
}

It calls mMainDataStreamer.update(), which fails and throws a KeyStoreException with code KM_ERROR_MEMORY_ALLOCATION_FAILED. But the exception is caught, stored in mCachedException, and null is returned. That's why you get null when you call update().

When you call doFinal(), it calls AndroidKeyStoreCipherSpiBase.engineDoFinal():

protected final byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen)
        throws IllegalBlockSizeException, BadPaddingException {
    if (mCachedException != null) {
        throw (IllegalBlockSizeException)
            new IllegalBlockSizeException().initCause(mCachedException);
    }

The method sees that there is a cached exception and throws it (wrapped in an IllegalBlockSizeException, which is totally unrelated to the real issue).

Keystore error

Now, the real issue. The actual work of encryption/decryption is performed by the Keystore service, which is a separate process written in C++. The relevant part for AES is in aes_operation.cpp.

There is a number of KM_ERROR_MEMORY_ALLOCATION_FAILED errors returned in that file. As its name suggests, the code means that a memory allocation has failed. So it seems that, for some reason, the Keystore was unable to allocate a buffer. It's hard to understand why.

Conclusion

Since the real cause is mysterious, I would suggest keeping a small buffer size and changing the splitting procedure as described at the beginning.

huangapple
  • 本文由 发表于 2020年4月8日 18:37:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/61098717.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定