В этом документе формально определяется математическая функция, представленная ключами потоковой передачи AES-GCM-HKDF (закодированными в прототипном формате как type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey
).
Это шифрование во многом основано на [HRRV15] 1 . Для анализа безопасности мы обращаемся к [HS20] 2 .
Ключ и параметры
Ключи описываются следующими частями (все размеры в этом документе указаны в байтах):
- \(\mathrm{KeyValue}\)— байтовая строка.
- \(\mathrm{CiphertextSegmentSize} \in \{1, 2, \ldots, 2^{31}-1\}\).
- \(\mathrm{DerivedKeySize} \in \{16, 32\}\).
- \(\mathrm{HkdfHashType} \in \{\mathrm{SHA1}, \mathrm{SHA256}, \mathrm{SHA512}\}\).
Действительные ключи дополнительно удовлетворяют следующим свойствам:
- \(\mathrm{len}(\mathrm{KeyValue}) \geq \mathrm{DerivedKeySize}\).
- \(\mathrm{CiphertextSegmentSize} > \mathrm{DerivedKeySize} + 24\) (это эквивалентно\(\mathrm{len}(\mathrm{Header}) + 16\) , как будет объяснено позже).
Ключи, которые не удовлетворяют ни одному из этих свойств, отклоняются Tink (либо при анализе ключа, либо при создании соответствующего примитива).
Функция шифрования
Чтобы зашифровать сообщение \(\mathrm{Msg}\) с соответствующими данными\(\mathrm{AssociatedData}\), мы создаем заголовок, разбиваем сообщение на сегменты, шифруем каждый сегмент и объединяем сегменты. Мы объясним эти шаги ниже.
Создание заголовка
Чтобы создать заголовок, мы сначала выбираем однородную случайную строку \(\mathrm{Salt}\)длины \(\mathrm{DerivedKeySize}\). Затем мы выбираем однородную случайную строку\(\mathrm{NoncePrefix}\) длины 7.
Затем мы устанавливаем\(\mathrm{Header} := \mathrm{len}(\mathrm{Header}) \| \mathrm{Salt} \| \mathrm{NoncePrefix}\), где длина заголовка кодируется как один байт. Обратите внимание, что\(\mathrm{len}(\mathrm{Header}) \in \{24, 40\}\).
Далее мы используем HKDF 3 с хэш-функцией, заданной \(\mathrm{HkdfHashType}\), и входными данными \(\mathrm{ikm} := \mathrm{KeyValue}\),\(\mathrm{salt} := \mathrm{Salt}\)и\(\mathrm{info} := \mathrm{AssociatedData}\)с выходной длиной \(\mathrm{DerivedKeySize}\). Мы называем результат \(k\).
Разделение сообщения
Сообщение \(\mathrm{Msg}\) затем разбивается на части:\(\mathrm{Msg} = M_0 \| M_1 \| \cdots \| M_{n-1}\).
Их длины выбираются так, чтобы они удовлетворяли:
- \(\mathrm{len}(M_0) \in \{0,\ldots, \mathrm{CiphertextSegmentSize} - \mathrm{len}(\mathrm{Header}) - \mathrm{16}\}\).
- Если \(n>1\), то \(\mathrm{len}(M_1), \ldots, \mathrm{len}(M_{n-1}) \in \{1,\ldots, \mathrm{CiphertextSegmentSize} - \mathrm{16}\}\).
- Если \(n>1\), то \(\mathrm{len}(M_{0}), \ldots, \mathrm{len}(M_{n-2})\) должен иметь максимальную длину в соответствии с указанными выше ограничениями.
В этом разбиении \(n\) может быть максимум \(2^{32}\). В противном случае шифрование не удастся.
Шифрование блоков
Чтобы зашифровать сегмент \(M_i\), мы сначала вычисляем\(\mathrm{IV}_i := \mathrm{NoncePrefix} \| \mathrm{i} \| b\), где мы кодируем\(\mathrm{i}\) в 4 байтах, используя кодировку с прямым порядком байтов, и устанавливаем байт $b$ равным 0x00
если $i < n-1$, и 0x01
в противном случае. .
Затем мы шифруем \(M_i\) с помощью AES GCM 4 , где ключ —\(\mathrm{DerivedKey}\), вектор инициализации — \(\mathrm{IV}_i\), а связанные данные \(A\) — пустая строка. Мы устанавливаем \(C_i\) как результат этого шифрования (т. е. объединение \(C\) и \(T\) в приведенной выше ссылке).
Объедините сегменты
Наконец, все сегменты объединяются в\(\mathrm{Header} \| C_0 \| \cdots \| C_{n-1}\), который является окончательным зашифрованным текстом.
Расшифровка
Расшифровка просто инвертирует шифрование. Мы используем заголовок для получения nonce и расшифровываем каждый сегмент зашифрованного текста индивидуально.
API-интерфейсы могут (и обычно так и делают) разрешать произвольный доступ или доступ к началу файла без проверки конца файла. Это сделано намеренно, поскольку можно расшифровать \(M_i\) из \(C_i\), не расшифровывая все предыдущие и оставшиеся блоки зашифрованного текста.
Однако API должны быть осторожны, чтобы не позволить пользователям путать ошибки конца файла и ошибки расшифровки: в обоих случаях API, вероятно, должен вернуть ошибку, и игнорирование разницы может привести к тому, что злоумышленник сможет эффективно обрезать файлы.
Сериализация и парсинг ключей
Чтобы сериализовать ключ в формате «Tink Proto», мы сначала сопоставляем параметры очевидным образом с прототипом, указанным в aes_gcm_hkdf_streaming.proto . version
поля должна быть установлена на 0. Затем мы сериализуем ее, используя обычную сериализацию прототипа, и встраиваем полученную строку в значение поля прототипа KeyData . Мы устанавливаем для поля type_url
type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey
. Затем мы устанавливаем key_material_type
в SYMMETRIC
и встраиваем его в набор ключей. Обычно мы устанавливаем для параметра output_prefix_type
значение RAW
. Исключением является то, что если ключ был проанализирован с другим значением, установленным для output_prefix_type
, Tink может записать либо RAW
, либо предыдущее значение.
Чтобы разобрать ключ, мы обращаем описанный выше процесс в обратном порядке (обычным способом при разборе прото). Поле key_material_type
игнорируется. Значение output_prefix_type
можно либо игнорировать, либо ключи, у которых output_prefix_type
отличается от RAW
, могут быть отклонены. Ключи, version
которых отличается от 0, должны быть отклонены.
Известные вопросы
Ожидается, что реализации вышеупомянутой функции шифрования не будут безопасными для вилки. См. Безопасность вилки .
Рекомендации
[HRRV15] Хоанг, Рейханитабар, Рогавей, Визар. Онлайн-шифрование с аутентификацией и его устойчивость к неправильному использованию при повторном использовании. КРИПТО 2015. https://eprint.iacr.org/2015/189 ↩
[HS20] Безопасность потокового шифрования в библиотеке Google Tink. Хоанг, Шен, 2020 г. https://eprint.iacr.org/2020/1019 ↩
[HKDF] Функция извлечения и расширения ключей на основе HMAC (HKDF), RFC 5869. https://www.rfc-editor.org/rfc/rfc5869 ↩
NIST SP 800-38D, Рекомендации по режимам работы блочного шифрования: режим Галуа/счетчика (GCM) и GMAC. ↩