Artykuł techniczny

Dlaczego Excel odrzuca zaszyfrowany skoroszyt: ECB i RC4

Zapisujesz skoroszyt, szyfrujesz go hasłem, przekazujesz plik koledze, a kolega otwiera go w programie Excel. Excel prosi o hasło. Kolega wpisuje je, a Excel je akceptuje. Do tego momentu szyfrowanie wygląda na poprawne. Następnie Excel wyświetla okno dialogowe informujące, że plik jest uszkodzony i nie można go otworzyć, lub otwiera go jako arkusz bezużytecznych komórek. Hasło było prawidłowe. Plik mimo to jest uszkodzony. Jest to najbardziej dezorientujący tryb awarii w szyfrowaniu pakietu Office, ponieważ część sprawdzająca hasło i część przechowująca dane są chronione przez dwie różne operacje, a poprawne wykonanie jednej z nich nie gwarantuje poprawności drugiej

Obie pułapki opisane tutaj mają dokładnie ten sam kształt. W każdym przypadku weryfikator przechodzi, a korpus nie, co wysyła cię na polowanie na problem z hasłem albo wyprowadzaniem klucza, którego tam nie ma. Prawdziwa usterka siedzi dalej, w tym, jak przekształcane są bajty pakietu. Te dwie pułapki są niezależne — jedna na ścieżce AES, druga na ścieżce RC4 — ale dzielą problem diagnostyczny, więc warto zobaczyć, czemu wynik poprawny w połowie jest najtrudniejszy do odczytania

Dlaczego pomyślne hasło niczego nie dowodzi w kwestii korpusu

Formatem używanym przez nowoczesne, zaszyfrowane pliki XLSX jest Szyfrowanie Standardowe ECMA-376 (Standard Encryption), które przechowuje obok siebie dwie zaszyfrowane rzeczy. Jedną z nich jest EncryptionVerifier: mały blok zawierający losową wartość oraz skrót (hash) tej wartości, zaszyfrowany za pomocą klucza wygenerowanego z hasła. Drugą rzeczą jest EncryptedPackage: cały kontener zip skoroszytu, zaszyfrowany tym samym kluczem. Weryfikator istnieje po to, by czytnik mógł potwierdzić hasło przed zaangażowaniem mocy obliczeniowej w odszyfrowanie megabajtów danych. Odszyfruj weryfikator, oblicz skrót losowej wartości, porównaj go z zapisanym skrótem – jeśli pasują, hasło jest prawidłowe

Pułapka polega na tym, że weryfikator i pakiet są szyfrowane oddzielnymi wywołaniami na osobnych buforach. Klucz, który został prawidłowo wygenerowany, odszyfruje weryfikator poprawnie bez względu na to, co stanie się później z pakietem. Jeśli więc generowanie klucza jest poprawne, ale transformacja pakietu błędna, Excel potwierdzi hasło z weryfikatora, a następnie wyłoży się na korpusie danych. Objaw wygląda jako "prawidłowe hasło, uszkodzony plik", co kieruje śledztwo na ścieżkę hasła, która jako jedyna nigdy nie była uszkodzona. Ten sam podział dotyczy starszego przypadku z szyfrem RC4: skrót weryfikatora jest sprawdzany jako pierwszy, a korpus dryfujący poza fazę nadal pozostawia to sprawdzenie nienaruszonym

Diagram Standard Encryption ECMA-376 w obsłudze HotXLS w Delphi: jeden wyprowadzony klucz chroni mały weryfikator i cały EncryptedPackage przez osobne wywołania
Weryfikator i pakiet są szyfrowane osobnymi wywołaniami na osobnych buforach, więc poprawne hasło nie dowodzi niczego o treści

Pułapka pierwsza: AES w ECB, a nie CBC

Specyfikacja [MS-OFFCRYPTO] §2.3.4.15 określa, że szyfrowanie standardowe zabezpiecza pakiet za pomocą algorytmu AES w trybie Electronic Codebook (ECB). Każdy 16-bajtowy blok dopełnionego pakietu jest szyfrowany niezależnie tym samym kluczem. Nie ma powiązania (chaining) między blokami ani wektora inicjalizacyjnego. Jest to nietypowy wybór według nowoczesnych standardów, gdzie tryb ECB jest zazwyczaj unikany, ale interoperacyjność to nie miejsce na kwestionowanie specyfikacji. Excel odszyfrowuje pakiet w trybie ECB, więc generator musi go szyfrować w ECB, inaczej obie strony się nie porozumieją

Kuszącym błędem jest szyfrowanie pakietu AES-em w trybie CBC z wektorem inicjującym z samych zer. Oto czemu to prawie działa i czemu „prawie” to najgorsze miejsce, w którym można wylądować. W CBC pierwszy blok tekstu jawnego jest przed szyfrowaniem xorowany z IV. Gdy IV składa się z samych zer, ten xor niczego nie zmienia, więc pierwszy blok CBC-z-zerowym-IV daje dokładnie ten sam szyfrogram co ECB. Od drugiego bloku CBC wpina poprzedni blok szyfrogramu do następnego, więc każdy blok po pierwszym odbiega od ECB

Teraz nałóż to na strukturę. Układ pakietu umieszcza 8-bajtowy prefiks długości little-endian na samym początku, więc części pliku sprawdzane przez Excel najwcześniej leżą w pierwszym bloku lub dwóch. Pierwszy blok, który przypadkiem pasuje, oznacza, że najwcześniejsza walidacja przechodzi, podczas gdy każdy późniejszy blok odszyfrowuje się do szumu. Leczenie nie jest subtelne, gdy tryb ma nazwę: szyfruj każdy 16-bajtowy blok ECB-em i nie łańcuchuj. W silniku XlsEncryptStdPackage przechodzi przez dopełniony bufor krokami po 16 bajtów i woła AESEncryptECB128Block na każdym z nich — ten sam prymityw, którego już użyto dla bloków weryfikatora. Źródła niosą komentarz przy pętli, który stawia sprawę jasno: CBC z zerowym IV zgadza się z ECB tylko dla pierwszego bloku, więc reszta pakietu odszyfrowałaby się do śmieci, a Excel odrzuciłby plik

Diagram HotXLS AES ECB kontra CBC z zerowym IV w poprzek 16-bajtowych bloków zaszyfrowanego pakietu XLSX, gdzie tylko pierwszy blok pasuje, a każdy późniejszy rozjeżdża się
CBC z zerowym IV odtwarza ECB tylko dla pierwszego bloku, który trzyma prefiks długości walidowany przez Excel najwcześniej, podczas gdy każdy późniejszy blok dekryptuje się w szum
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted serializuje skoroszyt, a następnie uruchamia
    // potok Standard Encryption ECMA-376: AES-128 ECB przez
    // pakiet według [MS-OFFCRYPTO] 2.3.4.15. Zwraca 1 przy sukcesie.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Pułapka druga: ponowne kluczowanie RC4 rozjeżdża się w fazie

Starsza ścieżka plików .xls wykorzystuje schemat RC4 CryptoAPI, a jego reguła jest inna. Specyfikacja [MS-OFFCRYPTO] §2.3.6 określa, że szyfr jest ponownie inicjowany kluczem przy każdej granicy bloku o rozmiarze 1024 bajtów. Strumień jest dzielony na bloki po 1024 bajty, świeży klucz RC4 jest generowany dla bloku numer 0, 1, 2 i tak dalej, a wewnątrz każdego bloku strumień klucza (keystream) jest konsumowany w sposób ciągły z bajtu na bajt. Dwa niezmienniki muszą zachodzić jednocześnie: ponowna inicjalizacja klucza na każdej granicy oraz konsumpcja strumienia klucza bez przerw wewnątrz bloku. RC4 to szyfr strumieniowy, więc jego strumień klucza to pojedyncza uporządkowana sekwencja; n-ty pobierany bajt jest określony przez liczbę bajtów pobranych wcześniej. Deszyfrowanie to ta sama operacja XOR na tej samej sekwencji, co oznacza, że generator i konsument muszą pobierać dokładnie te same bajty na dokładnie tych samych pozycjach

W tym cała trudność. Szyfr strumieniowy nie ma resynchronizacji. Jeśli zmarnujesz jeden bajt strumienia klucza, każdy następny bajt jest xorrowany z niewłaściwym bajtem strumienia klucza i błąd nigdy sam się nie poprawi; kaskadą idzie do końca bloku, a gdy bieżąca pozycja już jest zła, do każdego kolejnego bloku. Drobna wpadka w księgowaniu pozycji robi dokładnie to. Wyobraź sobie licznik bloków startujący z wartowniczej wartości minus jeden i procedurę pomijania, która zakłada, że licznik już pasuje do bieżącego bloku. Startując z tego wartownika, robi re-key i przepuszcza pełny 1024-bajtowy blok strumienia klucza, który nigdy nie powinien zostać skonsumowany, i przy okazji sprowadza pozostały licznik na wartości ujemne. Od tego momentu odszyfrowywacz jest przesunięty w fazie o cały blok. Weryfikator, sprawdzany przed wszystkim tym, dalej przechodzi, więc hasło wygląda dobrze, podczas gdy każda komórka danych wychodzi jako śmieci

W HotXLS logika żyje w TXLSDecrypterRC4. Zarówno Skip, jak i Decrypt dzielą jedną pętlę: re-key tylko wtedy, gdy bieżąca pozycja przekracza granicę nowego bloku, gdzie indeks bloku to pozycja dzielona przez REKEY_BLOCK_SIZE (1024), a potem konsumuj do reszty bieżącego bloku i nic więcej. MakeKey jest wołany z indeksem bloku, nigdy ze starym albo wartowniczym, a pozycja przesuwa się o dokładną liczbę przetworzonych bajtów, więc Skip i Decrypt pozostają w fazie z producentem. Lekcja siedzi w najmniejszej jednostce: pojedynczy zmarnowany bajt to nie mały błąd w szyfrze strumieniowym — to całkowita utrata wszystkiego, co jest dalej

Diagram dryfu ponownego kluczowania RC4 w przestarzałym szyfrowaniu Excel czytanym z Delphi: producent ponownie kluczuje co 1024 bajty, podczas gdy konsument zaczynający od wartownika spala jeden blok strumienia kluczy i zostaje poza fazą
Szyfr strumieniowy nie ma resynchronizacji, więc jeden spalony blok strumienia klucza 1024 bajtów zostawia każdą odszyfrowaną komórkę jako śmieci, podczas gdy weryfikator dalej przechodzi
var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create;
  try
    // Starszy .xls szyfrowany RC4 albo RC4 CryptoAPI odszyfrowuje się wewnątrz
    // Open: podaj hasło, a strumieniowy odszyfrowywacz trzyma rytm re-key
    // zgodny z producentem.
    Book.Open('legacy.xls', 'S3cret!');
    // tutaj czytaj komórki
  finally
    Book.Free;
  end;
end;

Zgodność z zamrożoną specyfikacją to dopasowanie co do bajtu

Obie pułapki sprowadzają się do tej samej zasady bazowej i warto ją wypowiedzieć osobno, bo zmienia, jak ważysz decyzje projektowe. Gdy konsumentem twojego wyjścia jest stały, zewnętrzny program, którego nie zmienisz, tryb szyfru i rytm ponownego kluczowania nie są szczegółami implementacji, które możesz optymalizować albo upraszczać. Są częścią kontraktu na przewodzie. Excel będzie odszyfrowywał ECB-em i robił re-key na granicach 1024 bajtów, niezależnie od tego, czy te wybory ci się podobają, a twoim jedynym zadaniem jest wyprodukowanie bajtów, które odszyfrowują się do oryginału dokładnie tą procedurą. Tryb nowocześniejszy, IV, który wydaje się nieszkodliwy, licznik startujący tam, gdzie naturalne; każde z tego jest wadą od momentu, w którym odbiega od oczekiwań czytnika. Zgodność z zamrożoną specyfikacją nie jest przybliżona. Jest dokładna co do bajtu albo nie działa wcale

Z tego powodu weryfikator hasła jest słabym testem jednostkowym na własną rękę. Mówi tylko, że generowanie klucza działa, co jest konieczne, ale niewystarczające. Test, który tylko otwiera zaszyfrowany plik i potwierdza pomyślne hasło, zaraportuje sukces, podczas gdy korpus danych pozostanie nieczytelny. Prawdziwy test odszyfrowuje pakiet i porównuje odzyskane bajty z oryginalnymi danymi wejściowymi lub przetwarza skoroszyt w obie strony przez szyfrowanie i deszyfrowanie, a następnie odczytuje komórki. Weryfikator dowodzi hasła; tylko korpus dowodzi szyfrowania

Obsługiwany sposób odczytu i zapisu chronionych skoroszytów

Publiczna powierzchnia jest mała. Żeby zapisać chroniony hasłem nowoczesny skoroszyt, wypełnij albo otwórz obiekt TXLSXWorkbook i wołaj SaveAsEncrypted z nazwą pliku i hasłem; serializuje skoroszyt i puszcza potok Standard Encryption opisany wyżej, zwracając 1 przy sukcesie. SaveAsEncryptedAgile bierze te same argumenty i zapisuje schemat Agile, którego domyślnie używa Excel 2010 i nowsze, z wyprowadzaniem klucza SHA-512 i szyfrowaniem pakietu AES-256. Do odczytu wołaj CanReadEncrypted, żeby sprawdzić, czy plik jest kontenerem Compound File, a potem rozgałęzij: OpenEncrypted odszyfrowuje pakiety Standard i Agile, a dla zwykłych plików spada do Open, i podnosi EXlsxEncryptionNotImplemented, gdy hasło jest złe albo kontener używa innego schematu. Starszy plik .xls też jest Compound File, więc idzie do TXLSWorkbook.Open z hasłem. Obsługa trybów i pętla re-key opisane wyżej siedzą pod tymi wołaniami; podajesz hasło i nazwę pliku, a silnik dopasowuje specyfikację za ciebie

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // Otwórz ponownie po stronie konsumenta
    Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
  finally
    Book.Free;
  end;
end;

Kształt chronionego wyjścia, strumień EncryptionInfo, bloki weryfikatora oraz układ pakietu zostały omówione w naszym artykule na temat chronionego wyjścia XLSX AES. Dla osobnego pytania o blokowanie na poziomie arkusza i o to, jak ochrona współgra z konfiguracją strony i drukowaniem, zobacz artykuł na temat ochrony, konfiguracji strony i drukowania. Oba te rozwiązania bazują na opisanej tutaj ścieżce szyfrowania, która jest dostarczana jako część komponentu HotXLS Delphi spreadsheet component dla Delphi i C++Builder, obok interfejsów API do odczytu, zapisu i renderowania omówionych w innych miejscach tego bloga