Technical Article

Proč Excel odmítá váš šifrovaný sešit: ECB a RC4

Napíšete sešit, zašifrujete jej heslem, předáte soubor kolegovi a ten jej otevře v Excelu. Excel si vyžádá heslo. Kolega jej zadá a Excel jej přijme. Zpočátku šifrování vypadá správně. Poté však Excel zobrazí dialog s informací, že soubor je poškozen a nelze jej otevřít, případně se otevře s listem plným nesmyslných buněk. Heslo bylo správné. Soubor je přesto poškozen. Jedná se o nejvíce matoucí stav selhání v šifrování Office, protože část, která vám říká, že heslo je správné, a část, která nese vaše data, jsou chráněny dvěma různými operacemi. Správnost jedné z nich nijak nezaručuje funkčnost té druhé.

Obě chyby popsané v tomto článku měly přesně tuto podobu. V obou případech ověřovací mechanismus (verifier) prošel, ale tělo dokumentu nikoli, což vás nutí hledat chybu v hesle nebo odvozování klíče, která tam přitom není. Skutečná chyba se nacházela níže na cestě, ve způsobu transformace bajtů balíčku. Obě chyby jsou nezávislé (jedna v cestě AES a druhá v cestě RC4), ale sdílejí stejný problém s diagnostikou. Stojí proto za to podívat se, proč je napůl správný výsledek tím nejsložitějším pro analýzu.

Proč úspěšné ověření hesla nedokazuje nic o těle dokumentu

Formát, který používá moderní šifrovaný sešit XLSX, je ECMA-376 Standard Encryption a ukládá dvě šifrované věci vedle sebe. Jednou z nich je EncryptionVerifier: a malý blok obsahující náhodnou hodnotu a hash této hodnoty, zašifrovaný klíčem odvozeným z hesla. Druhou věcí je EncryptedPackage: celý kontejner zip sešitu zašifrovaný stejným klíčem. Ověřovací blok (verifier) existuje proto, aby čtečka mohla potvrdit heslo dříve, než vynaloží úsilí na zpracování megabajtů těla dokumentu. Dešifruje verifier, provede hash náhodné hodnoty, porovná jej s uloženým hashem a pokud se shodují, heslo je správné.

Past spočívá v tom, že verifier a balíček jsou šifrovány samostatnými voláními nad samostatnými buffery. Klíč, který je odvozen správně, dešifruje verifier bez ohledu na to, co se stane s balíčkem později. Pokud je tedy vaše odvození klíče v pořádku, ale transformace balíčku chybná, Excel potvrdí heslo z verifieru a poté selže na těle dokumentu. Symptom se projevuje jako „správné heslo, poškozený soubor“, což směřuje vyšetřování k cestě hesla – tedy jediné části, která přitom nebyla poškozena. Stejné pravidlo platí i pro starší variantu RC4: hash verifieru se kontroluje jako první a tělo, které se odchýlí z fáze, stále nechá tento test projít.

Chyba jedna: AES v režimu ECB, nikoli CBC

Specifikace [MS-OFFCRYPTO] §2.3.4.15 určuje, že Standard Encryption šifruje balíček pomocí AES v režimu Electronic Codebook (ECB). Každý 16bajtový blok balíčku s výplní (padded package) se šifruje samostatně stejným klíčem. Mezi bloky neprobíhá žádné řetězení a nepoužívá se žádný inicializační vektor. Z moderního pohledu je to neobvyklá volba, protože režimu ECB se běžně vyhýbáme, avšak kompatibilita (interoperabilita) není místem pro zpochybňování specifikace. Excel dešifruje balíček jako ECB, takže jej tvůrce musí jako ECB zašifrovat, jinak se neshodnou.

Chyba spočívala v tom, že balíček byl zašifrován pomocí AES v režimu CBC s inicializačním vektorem složeným ze samých nul. Zde je důvod, proč to téměř funguje a proč je „téměř“ tím nejhorším místem pro výsledek. V CBC se první blok otevřeného textu před šifrováním zkombinuje pomocí XOR s inicializačním vektorem. Když je IV nulový, XOR nic nezmění, takže první blok CBC s nulovým IV vytvoří přesně stejný šifrový text jako režim ECB. Od druhého bloku dále však CBC přivádí předchozí blok šifrového textu do dalšího, takže každý blok po prvním se od ECB odchyluje.

Nyní se podívejme na strukturu. Rozvržení balíčku umisťuje 8bajtový prefix délky na samotný začátek, takže části souboru, které Excel kontroluje nejdříve, leží v prvním nebo druhém bloku. První blok, který se náhodou shoduje, znamená, že nejranější validace projde, zatímco každý další blok se dešifruje jako šum. Náprava je po pojmenování režimu jasná: zašifrujte každý 16bajtový blok pomocí ECB a přestaňte řetězit. V enginu XlsEncryptStdPackage prochází buffer s výplní po 16bajtových krocích a na každém volá AESEncryptECB128Block, což je stejná pomocná funkce, která se již používá pro bloky verifieru. Zdrojový kód obsahuje u smyčky komentář, který to jasně vysvětluje: CBC s nulovým IV odpovídá ECB pouze u prvního bloku, zbytek balíčku by se dešifroval jako odpad a Excel by jej odmítl.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted serializes the workbook, then runs the
    // ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
    // package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Chyba dvě: odchylka opětovného klíčování RC4

Starší cesta pro soubory .xls využívá schéma CryptoAPI s šifrou RC4 a její pravidlo je odlišné. Specifikace [MS-OFFCRYPTO] §2.3.6 určuje, že šifra se znovu klíčuje (re-key) na každé hranici 1024bajtového bloku. Datový proud je rozdělen do bloků o velikosti 1024 bajtů, pro bloky číslo 0, 1, 2 atd. se odvodí nový klíč RC4 a uvnitř každého bloku se proud klíčů (keystream) spotřebovává souvisle od bajtu k bajtu. Musí platit dva předpoklady zároveň: znovu klíčovat na každé hranici a spotřebovávat keystream bez mezer uvnitř bloku. RC4 je proudová šifra, takže její keystream je jedna uspořádaná sekvence. To, který n-tý bajt odeberete, je dáno tím, kolik bajtů jste odebrali před ním. Dešifrování je stejný XOR vůči stejné sekvenci, což znamená, že tvůrce i spotřebitel must odebírat přesně stejné bajty na přesně stejných pozicích. V tom spočívá celá složitost. Proudová šifra nemá možnost synchronizace. Pokud vyplýtváte jeden bajt keystreamu, každý další bajt se zkombinuje pomocí XOR s nesprávným bajtem keystreamu a chyba se nikdy neopraví. Šíří se do konce bloku a jakmile je průběžná pozice špatná, tak i do každého dalšího bloku. Zde popsaná chyba dělala přesně to. Počítadlo bloků začínalo od hodnoty minus jedna a rutina pro přeskakování předpokládala, že počítadlo již odpovídá aktuálnímu bloku. Počínaje touto hodnotou znovu zašifrovala a spotřebovala celých 1024 bajtů keystreamu, které se nikdy spotřebovat neměly, a v tomto procesu posunula zbývající počet do záporných hodnot. Od té chvíle byla dešifrovací rutina o celý blok mimo fázi. Verifier, kontrolovaný předtím, stále prošel, takže heslo vypadalo správně, zatímco každá datová buňka se vrátila jako odpad. Opravená logika se nachází v TXLSDecrypterRC4. Jak Skip, tak Decrypt sdílejí jednu smyčku: znovu klíčovat pouze tehdy, když průběžná pozice překročí hranici nového bloku, kde index bloku je pozice dělená REKEY_BLOCK_SIZE (1024). Poté spotřebovat data pouze do konce aktuálního bloku. Funkce MakeKey se volá s indexem bloku, nikoli se starým nebo výchozím indexem, a pozice se posouvá o přesný počet zpracovaných bajtů, takže Skip a Decrypt zůstávají fázově zarovnány s tvůrcem. Poučení se skrývá v nejmenším detailu: jediný promarněný bajt není u proudové šifry malou chybou, ale úplnou ztrátou všeho, co následuje.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    // CanReadEncrypted checks the Compound File (OLE2) signature so
    // you can branch before attempting a normal Open. OpenEncrypted
    // routes plain files to Open and handles the encrypted container.
    if Book.CanReadEncrypted('legacy.xls') then
      Book.OpenEncrypted('legacy.xls', 'S3cret!')
    else
      Book.Open('legacy.xls');
    // read cells here
  finally
    Book.Free;
  end;
end;

Kompatibilita s pevně danou specifikací vyžaduje shodu na bajt

Obě chyby se redukují na stejný základní princip, který stojí za to uvést samostatně, protože mění pohled na volbu návrhu. Pokud je spotřebitelem vašeho výstupu pevný externí program, který nemůžete změnit, režim šifry a kadence klíčování nejsou implementačními detaily, které byste si mohli optimalizovat nebo zjednodušit. Jsou součástí přenosového kontraktu. Excel bude dešifrovat pomocí ECB a znovu klíčovat na hranicích 1024 bajtů bez ohledu na to, zda se vám tyto volby líbí. Vaším jediným úkolem je vytvořit bajty, které se za tohoto přesného postupu dešifrují do původní podoby. Režim, který je modernější, inicializační vektor, který se zdá neškodný, počítadlo začínající tam, kde se to zdá přirozené – cokoli z toho je chybou v okamžiku, kdy se odchýlí od toho, co čtečka očekává. Kompatibilita vůči pevné specifikaci není přibližná. Je buď přesná na bajt, nebo nefunkční. To je také důvod, proč je samotný verifier slabým testem. Říká vám, že odvození klíče funguje, což je nutné, ale zdaleka ne dostačující. Test, který pouze otevře šifrovaný soubor a potvrdí, že heslo prošlo, nahlásí úspěch, i když je tělo nečitelné. Skutečný test dešifruje balíček a porovná obnovené bajty s původním vstupem, nebo sešit protočí přes zašifrování a dešifrování a načte buňky zpět. Verifier prokazuje heslo, pouze tělo prokazuje šifrování.

Podporovaný způsob čtení a zápisu chráněných sešitů

Veřejné rozhraní je malé. Chcete-li zapsat heslem chráněný moderní sešit, naplňte nebo otevřete TXLSXWorkbook a zavolejte SaveAsEncrypted s názvem souboru a heslem. Metoda sešit serializuje a spustí šifrovací pipeline Standard Encryption, kterou první oprava napravila. Vrací 1 při úspěchu. Pro načtení zavolejte CanReadEncrypted a zjistěte, zda je soubor šifrovaným kontejnerem Compound File (OLE2), a poté se rozvětvte: OpenEncrypted řeší šifrovanou cestu a pro běžné soubory se vrací k Open. Open s heslem je k dispozici přímo. Zpracování režimů a smyčka opětovného klíčování popsané výše se nacházejí pod těmito voláními. Vy dodáte heslo a název souboru a engine specifikaci splní za vás.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // Reopen on the consumer side
    Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
  finally
    Book.Free;
  end;
end;

Podoba chráněného výstupu, stream EncryptionInfo, bloky verifieru a rozvržení balíčku jsou popsány v našem průvodci šifrovaným výstupem XLSX s AES. Pro samostatnou otázku zabezpečení na úrovni listu a jak ochrana interaguje s nastavením stránky a tiskem se podívejte na článek o ochraně, nastavení stránky a tisku. Oba postupy staví na šifrovací cestě popsané v tomto článku, která je dodávána jako součást HotXLS spreadsheet component pro Delphi a C++Builder společně s rozhraními API pro čtení, zápis a vykreslování popsanými jinde na tomto blogu.