AEAD de streaming HMAC AES-CTR

Neste documento, definimos formalmente a função matemática representada pelas chaves de streaming HMAC AES-CTR (codificadas no formato proto como type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey).

Essa criptografia é vagamente baseada em [HRRV15]1. Para uma análise da segurança, consulte [HS20]2. Observe também que os testes de várias linguagens do Tink têm um teste aes_ctr_hmac_streaming_key_test.py que contém test_manually_created_test_vector com um tutorial completo sobre como receber um texto criptografado.

Chave e parâmetros

As chaves são descritas pelas partes a seguir (todos os tamanhos neste documento são em bytes):

  • \(\mathrm{InitialKeyMaterial}\), uma string de bytes: o material da chave inicial.
  • \(\mathrm{CiphertextSegmentSize} \in \{1, 2, \ldots, 2^{31}-1\}\).
  • \(\mathrm{DerivedKeySize} \in \{16, 32\}\).
  • \(\mathrm{HkdfHashType} \in \{\mathrm{SHA1}, \mathrm{SHA256}, \mathrm{SHA512}\}\).
  • \(\mathrm{HmacHashType} \in \{\mathrm{SHA1}, \mathrm{SHA256}, \mathrm{SHA512}\}\).
  • \(\mathrm{HmacTagSize} \in \mathbb{N}\).

As chaves válidas também atendem às seguintes propriedades:

  • \(\mathrm{len}(\mathrm{InitialKeyMaterial}) \geq \mathrm{DerivedKeySize}\).
  • Se \(\mathrm{HmacHashType} = \mathrm{SHA1}\) então \(\mathrm{HmacTagSize} \in \{10, \ldots, 20\}\).
  • Se \(\mathrm{HmacHashType} = \mathrm{SHA256}\) então \(\mathrm{HmacTagSize} \in \{10, \ldots, 32\}\).
  • Se \(\mathrm{HmacHashType} = \mathrm{SHA512}\) então \(\mathrm{HmacTagSize} \in \{10, \ldots, 64\}\).
  • \(\mathrm{CiphertextSegmentSize} > \mathrm{DerivedKeySize} + \mathrm{HmacTagSize} + 8\) (Isso é igual a \(\mathrm{len}(\mathrm{Header}) + \mathrm{HmacTagSize}\) , conforme explicado posteriormente).

As chaves que não atendem a nenhuma dessas propriedades são rejeitadas pelo Tink (quando a chave é analisada ou quando o primitivo correspondente é criado).

Função de criptografia

Para criptografar uma mensagem \(\mathrm{Msg}\) com dados associados\(\mathrm{AssociatedData}\), criamos um cabeçalho, dividimos a mensagem em segmentos, criptografamos cada segmento e concatenamos os segmentos. Explicamos essas etapas a seguir.

Criar o cabeçalho

Para criar o cabeçalho, primeiro escolhemos uma string aleatória uniforme \(\mathrm{Salt}\) de comprimento \(\mathrm{DerivedKeySize}\). Em seguida, escolhemos uma string aleatória uniforme \(\mathrm{NoncePrefix}\) de comprimento 7.

Em seguida, definimos \(\mathrm{Header} := \mathrm{len}(\mathrm{Header}) \| \mathrm{Salt} \| \mathrm{NoncePrefix}\), em que o comprimento do cabeçalho é codificado como um único byte. Observamos que \(\mathrm{len}(\mathrm{Header}) \in \{24, 40\}\).

Em seguida, usamos HKDF3 com função hash \(\mathrm{HkdfHashType}\) para calcular o tamanho da chave \(\mathrm{DerivedKeySize} + 32\) para esta mensagem: \(k := \mathrm{HKDF}(\mathrm{InitialKeyMaterial}, \mathrm{Salt}, \mathrm{AssociatedData})\). As entradas são usadas nas respectivas entradas correspondentes de \(\mathrm{HKDF}\): \(\mathrm{InitialKeyMaterial}\) is \(\mathrm{ikm}\), \(\mathrm{Salt}\) é o sal e \(\mathrm{AssociatedData}\) é usado como \(\mathrm{info}\).

A string \(k\) é dividida em duas partes \(k_1 \| k_2 := k\), por exemplo, \(\mathrm{len}(k_1) = \mathrm{DerivedKeySize}\) e \(\mathrm{len}(k_2) = 32\).

Dividir a mensagem

A seguir, a mensagem \(M\) será dividida em partes: \(M = M_0 \| M_1 \| \cdots \| M_{n-1}\).

Os comprimentos são escolhidos de modo que satisfaçam:

  • \(\mathrm{len}(M_0) \in \{0,\ldots, \mathrm{CiphertextSegmentSize} - \mathrm{len}(\mathrm{Header}) - \mathrm{HmacTagSize}\}\).
  • Se \(n > 1\), então \(\mathrm{len}(M_1), \ldots, \mathrm{len}(M_{n-1}) \in \{1,\ldots, \mathrm{CiphertextSegmentSize} - \mathrm{HmacTagSize}\}\).
  • Se \(n > 1\), então \(M_{0}, \ldots, M_{n-2}\) precisa ter o comprimento máximo de acordo com o acima para restrições.

Nessa divisão, \(n\) pode ser no máximo \(2^{32}\). Caso contrário, a criptografia falhará.

Como criptografar os blocos

Para criptografar o segmento \(M_i\), primeiro calculamos \(\mathrm{IV}_i := \mathrm{NoncePrefix} \| \mathrm{i} \| b \| 0x00000000\), em que codificamos \(\mathrm{i}\) em 4 bytes usando a codificação big-endian, e definimos o byte $b$ como 0x00 se $i < n-1$ e 0x01 caso contrário.

Em seguida, criptografamos \(M_i\) usando a chave de CTR do AES \(k_1\)e o vetor de inicialização \(\mathrm{IV}_i\). Em outras palavras, as entradas para as invocações de AES são \(\mathrm{IV}_i, \mathrm{IV}_i + 1, \mathrm{IV}_i + 2, \ldots\), em que \(\mathrm{IV}_i\) é interpretado como um número inteiro big-endian. Isso gera \(C'_i\).

Calculamos a tag usando HMAC com a função de hash fornecida por \(\mathrm{HmacHashType}\) e com key \(k_2\) sobre a concatenação\(\mathrm{IV}_i \| C'_i\).

Em seguida, concatenamos o texto criptografado seguido pela tag para extrair \(C_i\).

Concatenar os segmentos

Por fim, todos os segmentos são concatenados como \(\mathrm{Header} \| C_0 \| \cdots \| C_{n-1}\), que é o texto criptografado final.

Função de descriptografia

A descriptografia simplesmente inverte a criptografia. Usamos o cabeçalho para extrair o valor de uso único e descriptografar cada segmento do texto criptografado individualmente.

As APIs podem permitir (e normalmente permitem) acesso aleatório ou ao início de um arquivo sem inspecionar o fim do arquivo. Isso é intencional, já que é possível descriptografar \(M_i\) de \(C_i\), sem descriptografar todos os blocos de texto criptografado anteriores e restantes.

No entanto, as APIs precisam ter cuidado para não permitir que os usuários confundam erros de fim de arquivo e descriptografia: em ambos os casos, a API provavelmente precisa retornar um erro, e ignorar a diferença pode fazer com que um adversário consiga truncar arquivos com eficácia.

Serialização e análise de chaves

Para serializar uma chave no formato "Tink Proto", primeiro mapeamos os parâmetros da maneira óbvia para o proto em aes_ctr_hmac_streaming.proto. O campo version precisa ser definido como 0. Em seguida, serializamos isso usando a serialização proto normal e incorporamos a string resultante no valor do campo de um proto KeyData. Definimos o campo type_url como type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey. Em seguida, definimos key_material_type como SYMMETRIC e o incorporamos a um conjunto de chaves. Normalmente, definimos o output_prefix_type como RAW. A exceção é que, se a chave tiver sido analisada com um valor diferente definido para output_prefix_type, o Tink poderá gravar RAW ou o valor anterior.

Para analisar uma chave, invertemos o processo acima (da maneira normal ao analisar protos). O campo key_material_type é ignorado. O valor de output_prefix_type pode ser ignorado, ou as chaves com output_prefix_type diferentes de RAW podem ser rejeitadas. As chaves com um version diferente de 0 serão rejeitadas.

Referências


  1. [HRRV15] Hoang, Reyhanitabar, Rogaway e Vizar. Criptografia autenticada on-line e a resistência ao uso indevido ao reutilizar o valor de uso único. CRYPTO 2015. https://eprint.iacr.org/2015/189 

  2. [HS20] Segurança da criptografia de streaming na biblioteca Tink do Google. Hoang, Shen, 2020. https://eprint.iacr.org/2020/1019 

  3. [HKDF] Função de derivação de chaves com extração e expansão baseada em HMAC (HKDF, na sigla em inglês), RFC 5869. https://www.rfc-editor.org/rfc/rfc5869