Strumieniowe przesyłanie AEAD HMAC AES-CTR

Ten dokument formalnie definiuje funkcję matematyczną reprezentowaną przez klucze AES-CTR HMAC Streaming (zakodowane w formacie proto jako type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey).

Ten algorytm szyfrowania jest luźno oparty na [HRRV15]1. Analizę bezpieczeństwa znajdziesz w [HS20]2. Pamiętaj też, że testy Tink dotyczące różnych języków zawierają test aes_ctr_hmac_streaming_key_test.py, który zawiera test_manually_created_test_vector z pełnym przewodnikiem dotyczącym uzyskiwania szyfrogramu.

Klucz i parametry

Klucze są opisane za pomocą tych części (wszystkie rozmiary w tym dokumencie są podawane w bajtach):

  • \(\mathrm{InitialKeyMaterial}\), ciąg bajtów: początkowy materiał klucza.
  • \(\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}\).

Prawidłowe klucze muszą też spełniać te właściwości:

  • \(\mathrm{len}(\mathrm{InitialKeyMaterial}) \geq \mathrm{DerivedKeySize}\).
  • Jeśli \(\mathrm{HmacHashType} = \mathrm{SHA1}\) to \(\mathrm{HmacTagSize} \in \{10, \ldots, 20\}\).
  • Jeśli \(\mathrm{HmacHashType} = \mathrm{SHA256}\) to \(\mathrm{HmacTagSize} \in \{10, \ldots, 32\}\).
  • Jeśli \(\mathrm{HmacHashType} = \mathrm{SHA512}\) to \(\mathrm{HmacTagSize} \in \{10, \ldots, 64\}\).
  • \(\mathrm{CiphertextSegmentSize} > \mathrm{DerivedKeySize} + \mathrm{HmacTagSize} + 8\) (jest to równe\(\mathrm{len}(\mathrm{Header}) + \mathrm{HmacTagSize}\) , jak wyjaśniono poniżej).

Klucze, które nie spełniają żadnej z tych właściwości, są odrzucane przez Tink (czy to podczas analizowania klucza, czy tworzenia odpowiedniego prymitywu).

Funkcja szyfrowania

Aby zaszyfrować wiadomość \(\mathrm{Msg}\) z powiązanymi danymi,\(\mathrm{AssociatedData}\)tworzymy nagłówek, dzielimy wiadomość na segmenty, szyfrujemy każdy segment i konkatenujemy segmenty. Wyjaśniamy je poniżej.

Tworzenie nagłówka

Aby utworzyć nagłówek, najpierw wybieramy losowy ciąg znaków \(\mathrm{Salt}\)o długości \(\mathrm{DerivedKeySize}\). Następnie wybieramy losowy ciąg znaków o długości 7.\(\mathrm{NoncePrefix}\)

Następnie ustawiamy parametr\(\mathrm{Header} := \mathrm{len}(\mathrm{Header}) \| \mathrm{Salt} \| \mathrm{NoncePrefix}\), w którym długość nagłówka jest kodowana jako pojedynczy bajt. Zauważyliśmy, że \(\mathrm{len}(\mathrm{Header}) \in \{24, 40\}\).

Następnie używamy funkcji HKDF3 z funkcją skracania \(\mathrm{HkdfHashType}\), aby obliczyć materiał klucza o długości \(\mathrm{DerivedKeySize} + 32\) dla tej wiadomości: \(k := \mathrm{HKDF}(\mathrm{InitialKeyMaterial}, \mathrm{Salt}, \mathrm{AssociatedData})\). Dane wejściowe są używane w odpowiednich danych wejściowych funkcji\(\mathrm{HKDF}\): \(\mathrm{InitialKeyMaterial}\) to \(\mathrm{ikm}\),\(\mathrm{Salt}\) to sól, a \(\mathrm{AssociatedData}\) to \(\mathrm{info}\).

Ciąg znaków \(k\) jest następnie dzielony na 2 części \(k_1 \| k_2 := k\), tak aby\(\mathrm{len}(k_1) = \mathrm{DerivedKeySize}\) i  \(\mathrm{len}(k_2) = 32\).

Dzielenie wiadomości

Następnie wiadomość \(M\) zostaje podzielona na części: \(M = M_0 \| M_1 \| \cdots \| M_{n-1}\).

Ich długości są wybierane tak, aby spełniały te kryteria:

  • \(\mathrm{len}(M_0) \in \{0,\ldots, \mathrm{CiphertextSegmentSize} - \mathrm{len}(\mathrm{Header}) - \mathrm{HmacTagSize}\}\).
  • Jeśli \(n > 1\), to \(\mathrm{len}(M_1), \ldots, \mathrm{len}(M_{n-1}) \in \{1,\ldots, \mathrm{CiphertextSegmentSize} - \mathrm{HmacTagSize}\}\).
  • Jeśli \(n > 1\), pole \(M_{0}, \ldots, M_{n-2}\) musi mieć maksymalną długość zgodnie z powyższymi ograniczeniami.

W tym przypadku \(n\) może mieć maksymalnie wartość \(2^{32}\). W przeciwnym razie szyfrowanie się nie powiedzie.

Szyfrowanie bloków

Aby zaszyfrować segment \(M_i\), najpierw obliczamy\(\mathrm{IV}_i := \mathrm{NoncePrefix} \| \mathrm{i} \| b \| 0x00000000\), gdzie kodujemy \(\mathrm{i}\) w 4 bajtach za pomocą kodowania big-endian, a następnie ustawiamy bajt $b$ na 0x00, jeśli $i < n-1$ i 0x01 w przeciwnym razie.

Następnie szyfrujemy \(M_i\) za pomocą klucza AES CTR \(k_1\)i wektora inicjującego\(\mathrm{IV}_i\). Inaczej mówiąc, dane wejściowe wywołań AES to:\(\mathrm{IV}_i, \mathrm{IV}_i + 1, \mathrm{IV}_i + 2, \ldots\)gdzie \(\mathrm{IV}_i\) jest interpretowany jako liczba całkowita w systemie dużych bajtów. Da to \(C'_i\).

Obliczamy tag za pomocą funkcji HMAC z funkcją haszowania określoną przez \(\mathrm{HmacHashType}\) i kluczem \(k_2\) nad konkatenacją\(\mathrm{IV}_i \| C'_i\).

Następnie łączymy tekst zaszyfrowany z tagiem, aby uzyskać wartość \(C_i\).

Łączenie segmentów

Na koniec wszystkie segmenty są łączone w postaci\(\mathrm{Header} \| C_0 \| \cdots \| C_{n-1}\), która jest ostatecznym tekstem zaszyfrowanym.

Funkcja odszyfrowywania

Odszyfrowanie polega na odwróceniu szyfrowania. Z nagłówka uzyskujemy liczbę losową, a poszczególne segmenty tekstu zaszyfrowanego odszyfrowujemy osobno.

Interfejsy API mogą (i zwykle tak jest) zezwalać na dostęp losowy lub dostęp do początku pliku bez sprawdzania jego końca. Jest to celowe działanie, ponieważ można odszyfrować \(M_i\) z \(C_i\)bez odszyfrowywania wszystkich poprzednich i pozostałych bloków szyfrogramu.

Interfejsy API powinny jednak zapobiegać myleniu przez użytkowników błędów końca pliku i błędów odszyfrowywania. W obu przypadkach interfejs API musi zwrócić błąd, a ignorowanie różnicy może pozwolić przeciwnikowi skutecznie obcinać pliki.

Serializacja i parsowanie kluczy

Aby serializować klucz w formacie „Tink Proto”, najpierw mapujemy parametry w oczywisty sposób na proto podany w pliku aes_ctr_hmac_streaming.proto. Wartość pola version musi wynosić 0. Następnie serializujemy to za pomocą zwykłej serializacji proto i umieszczamy powstały ciąg znaków w polu proto KeyData. W polu type_url ustawiamy wartość type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey. Następnie ustawiamy key_material_type na SYMMETRIC i osadzamy to w kluczu. Zwykle ustawiamy output_prefix_type na RAW. Wyjątkiem jest sytuacja, gdy klucz został przeanalizowany z inną wartością dla output_prefix_type. Tink może zapisać RAW lub poprzednią wartość.

Aby przeanalizować klucz, odwracamy powyższy proces (w zwykły sposób podczas analizowania protosów). Pole key_material_type jest ignorowane. Wartość parametru output_prefix_type może zostać zignorowana, a klucze, w których parametrze output_prefix_type ma wartość inną niż RAW, mogą zostać odrzucone. Klucze, których wartość version jest inna niż 0, będą odrzucane.

Pliki referencyjne


  1. [HRRV15] Hoang, Reyhanitabar, Rogaway, Vizar. szyfrowanie z uwierzytelnianiem online i odporność na ponowne użycie nonce. CRYPTO 2015. https://eprint.iacr.org/2015/189 

  2. [HS20] Bezpieczeństwo szyfrowania strumieniowego w bibliotece Tink Google. Hoang, Shen, 2020. https://eprint.iacr.org/2020/1019 

  3. [HKDF] Funkcja derywacji klucza „wyodrębnij i rozwiń” (HKDF) oparta na HMAC, RFC 5869. https://www.rfc-editor.org/rfc/rfc5869