Artykuł techniczny

Odczytywanie zaszyfrowanych plików Excel (Agile) w Delphi za pomocą HotXLS

HotXLS odczytuje pliki Excel zaszyfrowane metodą Agile — czyli zabezpieczone hasłem w sposób, który program Excel 2010 i każda nowsza wersja stosują domyślnie — za pomocą jednego wywołania: TXLSXWorkbook.OpenEncrypted. Komponent analizuje deskryptor szyfrowania XML, wyprowadza klucze z hasła za pomocą łańcucha skrótów SHA-512 z liczbą iteracji spinCount, weryfikuje hasło względem zaszyfrowanego weryfikatora, a następnie odszyfrowuje pakiet w 4096-bajtowych segmentach AES-CBC. Cały proces odbywa się bez instalowania programu Excel, bez technologii COM i bez zewnętrznych bibliotek kryptograficznych DLL

Ten artykuł dotyczy konkretnie odczytu szyfrowania Agile. Dwa powiązane zagadnienia omówiono w osobnych artykułach: interakcja ze starszymi schematami RC4 i XOR wewnątrz starych plików BIFF .xls jest opisana w artykule o interoperacyjności ECB i RC4, a tworzenie skoroszytów chronionych hasłem z szyfrowaniem ECMA-376 Standard Encryption jest omówione w artykule o generowaniu plików XLSX chronionych przez AES. W tym przypadku plik już istnieje, ktoś inny go zaszyfrował, a Twoim zadaniem jest go otworzyć

Scenariusz wymuszający to rozwiązanie jest znany każdemu, kto zajmuje się przetwarzaniem dokumentów. Usługa importu po stronie serwera przyjmuje przesyłane skoroszyty; na maszynie nie ma i nigdy nie będzie programu Excel; i pewnego ranka klient przesyła zupełnie zwyczajny plik .xlsx, który czytnik ZIP odrzuca, ponieważ w ogóle nie jest to plik ZIP. Klient zapisał go z hasłem. Od tego momentu Twój moduł ładujący musi albo rozumieć standard [MS-OFFCRYPTO], albo odrzucić plik z powrotem do użytkownika, który z własnego punktu widzenia nie zrobił niczego nadzwyczajnego

Czym jest szyfrowanie Agile w pliku Excel?

Szyfrowanie Agile to schemat ochrony hasłem zdefiniowany w specyfikacji [MS-OFFCRYPTO] §2.3.4.10 do §2.3.4.15, który Excel 2010 i nowsze wersje zapisują za każdym razem, gdy skoroszyt jest zabezpieczany hasłem. Zaszyfrowany plik nie jest już pakietem ZIP. Jest to kontener OLE Compound File Binary (CFB) przechowujący dwa strumienie: EncryptionInfo, który opisuje sposób wykonania szyfrowania, oraz EncryptedPackage, który jest rzeczywistym plikiem ZIP .xlsx zaszyfrowanym jako nieprzezroczysty blob. Sygnatura CFB (D0 CF 11 E0 A1 B1 1A E1) to ten sam znacznik, który noszą starsze pliki BIFF .xls, dlatego pliku o zmienionej nazwie lub zaszyfrowanego nie można sklasyfikować na podstawie samego rozszerzenia

To, co odróżnia Agile od poprzedników, to fakt, że strumień EncryptionInfo jest samopiszący się. Po 8-bajtowym prefiksie wersji, w której zarówno wersja główna, jak i pomocnicza mają wartość 4, strumień jest deskryptorem XML zakodowanym w UTF-8. Element keyData deklaruje szyfr (AES), tryb powiązania bloków (ChainingModeCBC), funkcję skrótu (SHA512), długość klucza w bitach, rozmiar bloku oraz sól zakodowaną w Base64. Element hasła keyEncryptor niesie ze sobą własną sól, liczbę iteracji spinCount oraz trzy ładunki Base64: encryptedVerifierHashInput, encryptedVerifierHashValue i encryptedKeyValue. Excel domyślnie zapisuje AES-256 z liczbą iteracji 100 000, jednak deskryptor może zadeklarować AES-128 lub AES-192, a HotXLS uwzględnia długość wskazaną w keyBits, zamiast zakładać z góry 256

Jeden punkt wejścia dla skoroszytów jawnych, Standard oraz Agile

Metoda TXLSXWorkbook.OpenEncrypted obsługuje wszystkie trzy stany, jakie może napotkać wywołujący: zwykły ZIP, szyfrowanie Standard oraz szyfrowanie Agile, dzięki czemu procedury obsługi przesyłania nie muszą klasyfikować plików przed ich załadowaniem. Metoda najpierw bada plik: jeśli nie ma sygnatury CFB, przekazuje zadanie do zwykłej ścieżki Open, a hasło jest po prostu ignorowane. Jeśli plik jest kontenerem CFB, próbuje najpierw szyfrowania ECMA-376 Standard Encryption, a gdy sygnatura wersji EncryptionInfo wskazuje na Agile 4.4, kieruje proces do potoku Agile. Wartością zwracaną w przypadku sukcesu jest 1, co stanowi taki sam kontrakt jak dla metody Open

var
  Wb: TXLSXWorkbook;
begin
  Wb := TXLSXWorkbook.Create;
  try
    // Works for plain .xlsx, Standard-encrypted and
    // Agile-encrypted files alike
    if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
      Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
  finally
    Wb.Free;
  end;
end;

Rozwiązanie rezerwowe dla niezaszyfrowanych danych ma większe znaczenie, niż mogłoby się wydawać. Importer wsadowy, który zawsze wywołuje OpenEncrypted, nie wymaga rozgałęziania kodu po stronie wywołującej: pliki, które nigdy nie były chronione, ładują się dokładnie tak samo jak wcześniej, a pliki zaszyfrowane są odszyfrowywane w miejscu, a następnie przekazywane do zwykłego czytnika ZIP jako strumień w pamięci. Do przetestowania jest jedna ścieżka kodu, a nie trzy

W jaki sposób hasło staje się kluczem AES?

Szyfrowanie Agile nigdy nie używa hasła bezpośrednio. HotXLS najpierw oblicza hasz iterowany: początkowym skrótem jest SHA-512 z soli hasła połączonej z bajtami UTF-16LE hasła, a następnie ten skrót jest haszowany ponownie spinCount razy, przy czym w każdej rundzie na początku poprzedniego skrótu dodawany jest 32-bitowy licznik iteracji w formacie little-endian. Przy domyślnej dla Excela liczbie iteracji wynoszącej 100 000 daje to sto tysięcy seryjnych wywołań SHA-512 dla jednej próby podania hasła, i o to właśnie chodzi. Liczba iteracji spowalnia ataki brute-force: uprawnionego użytkownika kosztuje to jednorazowo kilka milisekund, a atakującego słownikowo kosztuje te same kilka milisekund przy każdej próbie zgadnięcia

// [MS-OFFCRYPTO] iterated password hash:
//   H(0) = SHA-512(salt + UTF-16LE(password))
//   H(n) = SHA-512(LE32(n - 1) + H(n - 1)), repeated spinCount times
function AgilePasswordHash(const Password: WideString;
  const Salt: TBytes; SpinCount: Integer): TBytes;
var
  buf: TBytes;
  i: Integer;
begin
  Result := XlsSHA512(Concat(Salt, Utf16LEBytes(Password)));
  SetLength(buf, 4 + 64);
  for i := 0 to SpinCount - 1 do
  begin
    PutLE32(buf, 0, i);            // iteration counter, little-endian
    Move(Result[0], buf[4], 64);   // previous digest
    Result := XlsSHA512(buf);
  end;
end;

Skrót po iteracjach nadal nie jest kluczem. Wyprowadza się z niego trzy odrębne klucze poprzez ponowne haszowanie z doklejonym stałym 8-bajtowym kluczem bloku, po jednym dla każdego celu: FE A7 D2 76 3B 4B 9E 79 do odszyfrowania danych wejściowych weryfikatora, D7 AA 0F 6D 30 61 34 4E dla skrótu weryfikatora oraz 14 6E 0B E7 AB AC D0 D6 do odpakowania właściwego klucza pakietu. Każdy wynik SHA-512 jest obcinany do zadeklarowanej długości klucza i, zgodnie z [MS-OFFCRYPTO], uzupełniany bajtami 0x36 w teoretycznym przypadku, gdy skrót jest krótszy niż klucz. Ta sama reguła dopełniania wartością 0x36 obowiązuje, gdy sól hasła jest rozszerzana do rozmiaru bloku w celu użycia jako wektor inicjujący (IV) dla trybu CBC

Weryfikacja hasła i pułapka obcinania długości saltSize

HotXLS weryfikuje hasło przed rozpoczęciem pracy z pakietem, korzystając z pary weryfikatorów z deskryptora. Odszyfrowuje encryptedVerifierHashInput pierwszym pochodnym kluczem, oblicza skrót SHA-512 z wyniku, odszyfrowuje encryptedVerifierHashValue drugim pochodnym kluczem i porównuje oba skróty bajt po bajcie. Niezgodność oznacza błędne hasło, co jest zgłaszane jako odrębny wynik, a nie jako uszkodzony skoroszyt. Co najważniejsze, oznacza to, że treść pakietu nigdy nie jest odszyfrowywana złym kluczem, więc nie ma możliwości, aby błędne hasło wygenerowało pozornie poprawne, ale uszkodzone dane

W specyfikacji znajduje się szczegół, który łatwo przeoczyć. Standard [MS-OFFCRYPTO] §2.3.4.13 definiuje weryfikator jako saltSize bajtów losowych danych, gdzie saltSize to długość soli modułu szyfrującego klucz, a nie rozmiar bloku szyfru. Ponieważ szyfrogram AES-CBC jest wyrównany do bloku, odszyfrowane dane wejściowe weryfikatora powracają z dopełnieniem do wielokrotności 16 bajtów i przed haszowaniem muszą zostać przycięte z powrotem do długości saltSize. Excel zawsze zapisuje saltSize równe blockSize (obie wartości to 16), więc implementacja pomijająca to obcinanie przechodzi pomyślnie wszystkie testy na rzeczywistych plikach z Excela, ale zawodzi na pierwszym pliku od generatora, który dobrał inną długość soli. HotXLS obcina dane do rzeczywistej długości soli, ponieważ tak nakazuje specyfikacja, a fakt, że te dwie wartości są w praktyce równe, jest zbiegiem okoliczności, a nie regułą

W jaki sposób odszyfrowywany jest EncryptedPackage?

Strumień EncryptedPackage zaczyna się od 8-bajtowego rozmiaru tekstu jawnego w formacie little-endian, po którym następuje szyfrogram w 4096-bajtowych segmentach. HotXLS odszyfrowuje go segment po segmencie, używając nowego IV dla każdego z nich. Sam klucz pakietu nie jest wyprowadzany bezpośrednio z hasła: jest to losowy klucz pośredni, który program zapisujący zaszyfrował wewnątrz encryptedKeyValue, a HotXLS odkodowuje go za pomocą trzeciego pochodnego klucza, obcinając wynik do długości zadeklarowanej w keyData. Wektor inicjujący (IV) każdego segmentu to skrót SHA-512 z soli keyData połączonej z 32-bitowym indeksem segmentu w formacie little-endian, obcięty do rozmiaru bloku. Taka konstrukcja oznacza, że każdy 4096-bajtowy segment można odszyfrować niezależnie, co w teorii pozwala na swobodny dostęp, choć HotXLS odszyfrowuje cały pakiet do pamięci i przekazuje wynikowe bajty ZIP do zwykłego modułu ładującego XLSX

Zadeklarowany rozmiar tekstu jawnego wykonuje końcową część zadania. Wyjście AES-CBC jest wyrównane do bloków, więc ostatni segment zawiera do 15 bajtów dopełnienia, które nie należą do dokumentu; odszyfrowany bufor jest obcinany do wartości określonej w prefiksie rozmiaru, a wynik to dokładnie plik ZIP .xlsx, który zaszyfrował program Excel. HotXLS sprawdza poprawność tego prefiksu względem rzeczywistej długości strumienia przed odszyfrowaniem, więc ucięty plik lub sfałszowane pole rozmiaru kończą się błędem zamiast przepełnienia bufora

Raportowanie błędów i rzetelne granice

Tryby błędów są celowo od siebie oddzielone. Błędne hasło wywołuje wyjątek z jawnym komunikatem o niepoprawnym haśle, co pozwala na wyświetlenie monitu o ponowną próbę. Kontener CFB, którego deskryptor deklaruje algorytmy spoza obsługiwanego zestawu — czyli cokolwiek innego niż AES w trybie CBC z haszowaniem SHA-512 w deskryptorze Agile — lub kontener, który nie jest ani Standard, ani Agile, wywołuje inny wyjątek identyfikujący schemat jako nieobsługiwany. Tych dwóch sytuacji nie wolno mylić: ponowne wpisywanie hasła przy nieobsługiwanym schemacie to strata czasu użytkownika, a zgłaszanie błędnego hasła jako błędu formatu kieruje zespół wsparcia na fałszywy tor

function LoadUploadedWorkbook(const FileName: WideString;
  const Password: WideString; Wb: TXLSXWorkbook): Boolean;
begin
  Result := False;
  try
    Result := Wb.OpenEncrypted(FileName, Password) = 1;
  except
    on E: EXlsxEncryptionNotImplemented do
      // Raised for both a wrong password and an unsupported
      // scheme; E.Message states which, so log it verbatim and
      // only offer a password retry for the wrong-password case
      RejectUpload(FileName, E.Message);
  end;
end;

Warto jasno określić te granice. HotXLS odczytuje deskryptory Agile deklarujące algorytm AES w trybie CBC z funkcją SHA-512, co pokrywa to, co rzeczywiście zapisują wersje programu Excel od 2010 do Excel 365, we wszystkich trzech długościach klucza. Deskryptory wskazujące inne szyfry lub algorytmy skrótu są odrzucane, a szyfrowanie oparte na certyfikatach nie jest obsługiwane — przetwarzany jest wyłącznie moduł szyfrujący klucz za pomocą hasła. Po stronie zapisu HotXLS generuje obecnie szyfrowanie Standard Encryption, a nie Agile, co ma znaczenie, jeśli inne narzędzia badają schemat zabezpieczeń; szczegóły zawiera artykuł o generowaniu plików XLSX chronionych przez AES

Przesyłanie plików chronionych hasłem przestaje być problemem, gdy moduł ładujący traktuje szyfrowanie jako część formatu pliku, a nie jako wyjątek od reguły. Punkt wejścia OpenEncrypted, wyprowadzanie klucza SHA-512 z liczbą iteracji oraz segmentowany potok AES-CBC opisane tutaj są częścią komponentu HotXLS Delphi Excel Component, wraz z całym natywnym silnikiem odczytu i zapisu XLS oraz XLSX dla Delphi i C++Builder