Проблема с инкрементным шифрованием на указанном c Android устройстве / ОС - PullRequest
3 голосов
/ 08 апреля 2020

Я использую инкрементное шифрование в сочетании с 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 различных устройствах, и никогда не возникало никаких проблем, кроме как с одним телефоном (Xperia XA с Android 7.0). Для этого телефона, если вход (textToEncrypt) достаточно мал, чтобы все можно было зашифровать в одном фрагменте, это нормально, но если он больше (обычно около 100 КБ), так что ему нужно больше фрагментов, то он будет не сможет зашифровать данные. Вот что я получаю:

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, поэтому в моем коде я разрешаю возвращать ноль, и затем отбросьте их для формирования зашифрованных данных. Это означает, что cipher.doFinal должен возвращать за один go все зашифрованные данные.

РЕДАКТИРОВАТЬ: Так что, очевидно, только для этого телефона размер порции не подходит: он не может быть 32Кб, но 8Кб отлично работает

Ответы [ 2 ]

4 голосов
/ 17 апреля 2020

Расширяя ответ Оливье. Он решил, что исключение выдается mMainDataStreamer.update(). Если вы посмотрите на класс AndroidKeyStoreCipherSpiBase, вы увидите, что mMainDataStreamer является экземпляром класса KeyStoreCryptoOperationChunkedStreamer . Вот интересная часть:

// 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;

В нашем случае используется максимальный размер чанка по умолчанию. DEFAULT_MAX_CHUNK_SIZE устанавливает верхний предел размера чанка. Если вы передадите большие куски методу cipher.update(), они будут разрезаны на куски DEFAULT_MAX_CHUNK_SIZE. Как видите, даже разработчики Android не создали новый точный безопасный размер куска и должны были догадаться (безуспешно, в вашем случае).

Однако обратите внимание, что Binder буфер используется для передачи этих фрагментов процессу шифрования и получения от него результатов. И его размер составляет всего около 1 МБ.

Может быть, на этом конкретном устройстве есть необычно маленький буфер Binder? Вы можете попытаться разобраться в этом, используя этот ответ: { ссылка }

На будущих устройствах вы можете использовать:

IBinder.getSuggestedMaxIpcSizeBytes()

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

2 голосов
/ 11 апреля 2020

Я провел некоторое время, анализируя проблему. Я не смог найти истинную причину, но, по крайней мере, я нашел некоторые элементы, поэтому вот они.

Многоэтапная обработка

Сначала вы говорите, что используете размер чанка: 32Kb, но это не совсем так. Вы разделяете строку на куски по 32K c (32768 символов ), а затем конвертируете каждый кусок в байтовый массив. Поскольку представление символа в UTF-8 может варьироваться от 1 до 4 байтов, ваш байтовый массив обычно будет больше , чем 32 КБ (если у вас нет только символов ASCII).

Сначала вы должны преобразуйте строку в байтовый массив, затем разбейте ее на куски по 32 КБ. Только это гарантирует размер буфера, который вы передаете в крипто API.

Ошибка на стороне клиента

Теперь о полученной трассировке стека. Вопреки тому, что кажется на первый взгляд, ошибка возникает не в doFinal(), а в update(). Когда вы звоните update(), вызов делегируется на AndroidKeyStoreCipherSpiBase.engineUpdate(). Интересная часть:

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

Вызывает mMainDataStreamer.update(), что приводит к сбою и выбрасывает KeyStoreException с кодом KM_ERROR_MEMORY_ALLOCATION_FAILED. Но исключение перехватывается, сохраняется в mCachedException и возвращается null. Вот почему вы получаете null при вызове update().

Когда вы звоните 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, что совершенно не связано с реальной проблемой).

Ошибка хранилища ключей

Теперь настоящая проблема. Фактическая работа шифрования / дешифрования выполняется службой Keystore, которая является отдельным процессом, написанным на C ++. Соответствующая часть для AES находится в aes_operation. cpp.

В этом файле возвращено несколько KM_ERROR_MEMORY_ALLOCATION_FAILED ошибок. Как следует из названия, код означает, что выделение памяти не удалось. Похоже, что по какой-то причине хранилище ключей не смогло выделить буфер. Трудно понять, почему.

Заключение

Поскольку истинная причина загадочна, я бы предложил оставить небольшой размер буфера и , изменяя процедуру разделения, как описано в начале .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...