Symetryczne szyfry blokowe szyfrują wiadomości o określonej długości, która jest charakterystyczna dla wybranego algorytmu (np. 16 bajtów dla algorytmu AES, 8 bajtów dla algorytmu 3DES). Nazywamy je blokami. Jeśli blok jest zbyt krótki do przetworzenia przez algorytm kryptograficzny należy użyć dopełnienia (ang. padding). Jeśli wiadomość jest zbyt długa rozwiązaniem będzie podzielenie jej na bloki o odpowiedniej długości. Tryb działania (ang. mode of operation) szyfru blokowego to różne techniki łączenia bloków podczas operacji szyfrowania. Nazywane są również trybem pracy szyfru blokowego lub potocznie trybem szyfrowania. W poniższym wpisie przedstawię dwa z nich: tryb elektronicznej książki kodowej (ang. electronic code book, ECB) oraz tryb wiązania bloków szyfrogramu (ang. cipher block chaining, CBC). Oba tryby opisane zostały w standardzie FIPS 81.
Elektroniczna książka kodowa
Szyfrowanie w trybie elektronicznej książki kodowej polega na niezależnym szyfrowaniu każdego bloku. Stąd wywodzi się również nazwa tego trybu. Książka kodowa to zbiór pewnych fraz i odpowiadających im fraz po zakodowaniu (w tym przypadku zaszyfrowaniu). Jeśli dla danego klucza (a bloki z wiadomości szyfrujemy tym samym kluczem) utworzymy wszystkie możliwe teksty jawne i kryptogramy im odpowiadające otrzymamy książkę kodową. Książek kodowych będzie tyle ile możliwych kluczy dla danego szyfru. Aby szyfrować za pomocą danej książki kodowej należy znaleźć odpowiedni tekst jawny i odpowiadający jej kryptogram. Podobnie będzie przebiegała operacja deszyfrowania.
Poniższy przykład szyfruje i deszyfruje dwa bloki danych w trybie ECB. Zachęcam do uzupełnienia go o fragment kodu wypisujący na ekranie tekst jawny i kryptogram oraz do przyjrzenia się wynikom. Można skorzystać z metody printHexBinary(byte[] val)
klasy javax.xml.bind.DatatypeConverter
.
1import javax.crypto.Cipher; 2import javax.crypto.spec.SecretKeySpec; 3 4public class ECBTest { 5 public static void main(String[] args) throws Exception { 6 byte[] plain = "abcdefghijklmnopabcdefghijklmnop".getBytes(); 7 byte[] key = "klucz--128-bitow".getBytes(); 8 9 SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); 10 Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); 11 12 // szyfrowanie 13 cipher.init(Cipher.ENCRYPT_MODE, secretKey); 14 byte encrypted[] = cipher.doFinal(plain); 15 16 // deszyfrowanie 17 cipher.init(Cipher.DECRYPT_MODE, secretKey); 18 byte decrypted[] = cipher.doFinal(encrypted); 19 } 20}
Jeśli dwa razy tym samym kluczem zaszyfrujemy identyczne bloki to otrzymamy dwa identyczne szyfrogramy. Jest to zgodne z ideą książki kodowej. Oczywiście atakujący nie wie jaki był tekst jawny, ale już wie, że dwa razy zaszyfrowano taki sam blok. Jeśli jego wiedza o budowie tekstu jawnego będzie większa, może on w prosty sposób wykorzystać przekazywane szyfrogramy do ataku. Nie będzie jednak próbował ich zdeszyfrować a jedynie będzie manipulował ich kolejnością. Przyjmijmy, że kwota przelewu przekazywana jest w postaci zaszyfrowanej algorytmem AES jako 32 cyfry (2 bloki po 16 bajtów). Jest dużo bardziej prawdopodobne, że znaczące cyfry znajdują się w drugim bloku niż w pierwszym, ponieważ z reguły przelewy nie są wykonywane na tak duże kwoty. Atakujący nie jest w stanie zdeszyfrować kryptogramów, ale może zamienić je miejscami. Uzyska jeszcze więcej jeśli usunie pierwszy kryptogram i zastąpi go drugim. System odbierający dane rozszyfruje je prawidłowo (będą to cyfry), jednak kwota prawdopodobnie będzie znacząco wyższa. Atakujący manipulując kryptogramem dokonał udanego ataku. Bardzo istotne jest również to, że wykorzystując bezpieczny algorytm (AES) w niebezpieczny sposób (zły tryb działania) otrzymaliśmy niebezpieczny system! Warto samodzielnie spróbować dokonać takiego ataku modyfikując przykładowy program.
Innym dobrym przykładem jest obrazek tego pingwina, o którego poufności ciężko mówić po zaszyfrowaniu w trybie ECB. Dany kolor w jawnym obrazku po zaszyfrowaniu stał się innym kolorem, ale zawsze takim samym.
Czy powyższe przykłady wskazują jednoznacznie, że tryb ten jest niebezpieczny i nie powinno się go w ogóle używać? Moim zdaniem nie zawsze. Wszystko zależy od tego w jakich zastosowaniach jest używany i czy jesteśmy świadomi do jakich ataków może to doprowadzić. Dużo poważniejszym błędem niż wykorzystanie trybu ECB w pierwszym przykładzie ataku był brak zapewnienia uwierzytelnienia źródła danych.
Na problem bezpieczeństwa trybu ECB można spojrzeć jeszcze w inny sposób. Wykorzystując rozumowanie z poprzedniego artykułu związane z czarną skrzynką możemy wyobrazić sobie w niej szyfrator ECB. Czy atakujący może w prosty sposób odróżnić go od generatora liczb losowych przesyłając wiadomości i obserwując odpowiedzi? Może. Czytelnikom pozostawiam znalezienie przykładowych zapytań do takiej czarnej skrzynki umożliwiających efektywne rozróżnienie jej od generatora liczb losowych.
Wiązanie bloków szyfrogramu
Tryb działania z wiązaniem bloków szyfrogramu (wiązaniem bloków zaszyfrowanych) eliminuje podstawowe wady trybu elektronicznej książki kodowej tworząc zależność pomiędzy poszczególnymi blokami za pomocą operacji xor. Przed zaszyfrowaniem blok poddawany jest działaniu xor z poprzednim kryptogramem. Taka sama operacja wykonywana jest po rozszyfrowaniu co umożliwia odzyskanie tekstu jawnego, ponieważ dwukrotny xor z tymi samymi danymi usuwa przekształcenie przed zaszyfrowaniem przywracając oryginalny tekst jawny. Pierwszy blok nie ma poprzedzającego kryptogramu dlatego operacja xor wykonywana jest z tak zwanym wektorem początkowym (ang initialization vector, IV), zwanym również wektorem początkowym.
Program wykorzystywany wcześniej do szyfrowania w trybie ECB będzie wymagał kilku modyfikacji. Poza zmianą trybu niezbędne będzie wprowadzenie parametru reprezentującego wektor początkowy. Zawiera go instancja klasy IvParameterSpec
. Ponownie zachęcam do uzupełnienia kodu o fragment wypisujący na ekranie tekst jawny i kryptogram oraz do przyjrzenia się wynikom.
1import javax.crypto.Cipher; 2import javax.crypto.spec.SecretKeySpec; 3import javax.crypto.spec.IvParameterSpec; 4 5public class CBCTest { 6 public static void main(String[] args) throws Exception { 7 byte[] plain = "abcdefghijklmnopabcdefghijklmnop".getBytes(); 8 byte[] key = "klucz--128-bitow".getBytes(); 9 byte[] iv = "-blok-wektor-iv-".getBytes(); 10 11 SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); 12 IvParameterSpec ivps = new IvParameterSpec(iv); 13 Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); 14 15 // szyfrowanie 16 cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivps); 17 byte encrypted[] = cipher.doFinal(plain); 18 19 // deszyfrowanie 20 cipher.init(Cipher.DECRYPT_MODE, secretKey, ivps); 21 byte decrypted[] = cipher.doFinal(encrypted); 22 } 23}
Jaki powinien być wektor początkowy? Rozważając eksperyment z czarną skrzynką: jeśli wektor początkowy będzie przewidywalny lub wręcz za każdym razem taki sam (a to często zdarza się w działających systemach) można się go łatwo pozbyć odpowiednio preparując pierwszy blok. Przed zaszyfrowaniem wykonujemy xor z przewidywanym wektorem początkowym. Tryb wykona powtórny xor usuwając zmiany wprowadzone przez IV. W ten sposób zaszyfrowany zostanie pierwszy blok w swojej oryginalnej wartości, bez wpływu wektora inicjującego. Dlatego też wektor początkowy powinien być losowy (co w kryptografii implikuje założenie o jego unikalności). Nie ma przeciwwskazań aby wektor początkowy był przekazywany w formie jawnej. Można go traktować jako nieużywaną część kryptogramu (wektory inicjujące kolejnych bloków są poprzedzającymi kryptogramami, które przekazujemy w niezabezpieczonym kanale).
Zatem modyfikując powyższy przykład zamiast inicjować wektor IV tymi samymi danymi należałoby użyć tam danych losowych:
1 byte[] iv = new byte[16]; 2 new SecureRandom().nextBytes(iv);
Po takiej zmianie kryptogram każdorazowo będzie wyglądał inaczej. Pingwin, o którym była mowa wcześniej, zaszyfrowany w trybie CBC może wyglądać jak na tym rysunku. Jest już w pełni ukryty.
Czy poza tymi zaletami tryb CBC ma jakieś wady? Niestety tak. Nie ma wprawdzie możliwości dowolnego manipulowania tekstem jawnym poprzez modyfikacje w kryptogramie, ale łatwo usunąć jego początek (usuwając początek kryptogramu i podmieniając wektor początkowy) lub jego koniec (poprzez usunięcie końca kryptogramu). Można tego uniknąć przesyłając w kryptogramie długość całej oryginalnej wiadomości. Każdorazowo należy pamiętać o wspomnianej już losowości wektora inicjującego.
Podsumowanie
Tryby działania szyfrów blokowych wykorzystywane są, gdy szyfrujemy więcej niż pojedynczy blok danych. Odpowiednie dobranie trybu może mieć kluczowe znaczenie dla bezpieczeństwa, o czym przekonaliśmy się na kilku prostych przykładach. Należy zwrócić uwagę na to, że oba przedstawione tryby działania nie zapewniają integralności oraz uwierzytelnienia przesyłanych danych. Oczywiście można by uzupełnić je o niezależnie obliczany MAC. Istnieją tryby, które zostały zaprojektowane z myślą o tych niezbędnych usługach. Są to tak zwane tryby pracy szyfrów blokowych do realizacji uwierzytelnionego szyfrowania (ang. authenticated encryption, AE) oraz uwierzytelnionego szyfrowania z dodatkowymi danymi (ang. authenticated ecnryption with additional data, AEAD), w których zapewnione jest również uwierzytelnienie przesyłanych danych zaszyfrowanych i jawnych. Trybów pracy szyfrów blokowych jest całe mnóstwo, wiele z nich wraz z analizami bezpieczeństwa i aktualnymi zaleceniami wymienionych jest na stronach NIST.