Technical Article

Prečo Excel odmieta váš šifrovaný zošit: ECB a RC4

Napíšete zošit, zašifrujete ho heslom, odovzdáte súbor kolegovi a kolega ho otvorí v Exceli. Excel si vypýta heslo. Kolega ho zadá a Excel ho prijme. Zatiaľ šifrovanie vyzerá správne. Potom však Excel zobrazí dialógové okno, ktoré hovorí, že súbor je poškodený a nedá sa otvoriť, prípadne ho otvorí do hárku nezmyselných buniek. Heslo bolo správne. Súbor je napriek tomu rozbitý. Toto je ten najviac dezorientujúci režim zlyhania v šifrovaní Office, pretože časť, ktorá potvrdzuje správnosť hesla, a časť, ktorá drží vaše dáta, sú chránené dvoma odlišnými operáciami a to, že urobíte jednu správne, nijak nezaručuje druhú.

Obe tu popísané chyby mali presne tento tvar. V oboch prípadoch overovateľ (verifier) prešiel, ale telo nie, čo vás vedie k hľadaniu chyby v odvodzovaní kľúča alebo hesla, ktorá tam nie je. Skutočná chyba bola ďalej v reťazci – v tom, ako boli transformované bajty balíka. Tieto dve chyby sú nezávislé (jedna v ceste AES a druhá v ceste RC4), ale zdieľajú problém s diagnostikou, preto stojí za to pozrieť sa na to, prečo je napoly správny výsledok tým najťažšie čitateľným.

Prečo prechádzajúce heslo nedokazuje nič o tele

Formát, ktorý používa moderné šifrované XLSX, je ECMA-376 Standard Encryption a ukladá dve šifrované veci vedľa seba. Jednou je EncryptionVerifier – malý blok obsahujúci náhodnú hodnotu a hash tejto hodnoty, šifrovaný kľúčom odvodeným od hesla. Druhou je EncryptedPackage – celý kontajner zip zošita, šifrovaný rovnakým kľúčom. Overovateľ existuje preto, aby čítačka mohla potvrdiť heslo predtým, ako vynaloží úsilie na megabajty tela. Dešifrujte overovateľ, zahashujte náhodnú hodnotu, porovnajte ju s uloženým hashom a ak sa zhodujú, heslo je správne.

Pascou je, že overovateľ a balík sú šifrované samostatnými volaniami nad odlišnými vyrovnávacími pamäťami. Kľúč, ktorý je odvodený správne, dešifruje overovateľ správne bez ohľadu na to, čo sa potom stane s balíkom. Takže ak je odvodenie kľúča správne, ale transformácia balíka nesprávna, Excel potvrdí heslo z overovateľa a potom zlyhá na tele. Príznak sa javí ako „správne heslo, poškodený súbor“, čo smeruje vyšetrovanie na cestu hesla, ktorá však nikdy nebola poškodená. Rovnaké oddelenie riadi aj starší prípad RC4: najprv sa skontroluje hash overovateľa a telo, ktoré sa posunie mimo fázu, stále necháva túto kontrolu úspešnú.

Chyba jedna: AES v režime ECB, nie CBC

Špecifikácia [MS-OFFCRYPTO] §2.3.4.15 uvádza, že Standard Encryption šifruje balík pomocou AES v režime Electronic Codebook (ECB). Každý 16-bajtový blok zarovnaného balíka je šifrovaný nezávisle rovnakým kľúčom. Medzi blokmi neexistuje žiadne reťazenie a neexistuje inicializačný vektor. Ide o nezvyčajnú voľbu podľa moderných štandardov, kedy sa režimu ECB zvyčajne vyhýba, ale interoperabilita nie je miestom na spochybňovanie špecifikácie. Excel dešifruje balík ako ECB, takže tvorca ho musí šifrovať ako ECB, inak sa nezhodnú.

Chybou bolo, že balík bol šifrovaný pomocou AES v režime CBC s použitím inicializačného vektora zo samých núl. Tu je dôvod, prečo to takmer funguje a prečo je „takmer“ tým najhorším miestom na pristátie. V režime CBC sa prvý blok otvoreného textu pred šifrovaním sčíta operáciou XOR s IV. Keď je IV samá nula, tento XOR nič nezmení, takže prvý blok CBC-s-nulovým-IV produkuje presne rovnaký šifrovaný text ako ECB. Od druhého bloku ďalej CBC privádza predchádzajúci blok šifrovaného textu do nasledujúceho, takže každý blok po prvom sa líši od ECB.

Teraz to premietnime na štruktúru. Rozloženie balíka umiestňuje 8-bajtový dĺžkový prefix little-endian na samotný začiatok, takže časti súboru, ktoré Excel kontroluje najskôr, sedia v prvom alebo dvoch blokoch. Prvý blok, ktorý sa zhodou okolností zhoduje, znamená, že najskoršia validácia prejde, zatiaľ čo každý neskorší blok sa dešifruje ako šum. Oprava nie je zložitá, akonáhle sa pomenuje režim: zašifrovať každý 16-bajtový blok pomocou ECB a zastaviť reťazenie. V engine prechádza XlsEncryptStdPackage zarovnanú vyrovnávaciu pamäť po 16-bajtových krokoch a na každý z nich volá AESEncryptECB128Block, čo je rovnaké primitívum, aké sa už používa pre bloky overovateľa. Zdrojový kód nesie pri slučke komentár, ktorý jasne hovorí pravidlo: CBC s nulovým IV sa zhoduje s ECB iba pre prvý blok, takže zvyšok balíka by sa dešifroval ako odpad a Excel by ho odmietol.

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 dve: zmena kľúča RC4 sa posunie mimo fázu

Staršia cesta .xls používa schému RC4 CryptoAPI a jej pravidlo je iného druhu. Špecifikácia [MS-OFFCRYPTO] §2.3.6 určuje, že šifra mení kľúč (re-key) na každej hranici 1024-bajtového bloku. Stream je rozdelený na bloky s veľkosťou 1024 bajtov, nový kľúč RC4 sa odvodzuje pre bloky číslo 0, 1, 2 atď., a v rámci každého bloku sa keystream spotrebováva nepretržite od bajtu k bajtu. Spolu musia platiť dva invarianty: zmena kľúča na každej hranici a spotreba keystreamu bez medzier vnútri bloku. RC4 je prúdová šifra, takže jej keystream je jediná usporiadaná sekvencia; n-tý bajt, ktorý nakreslíte, je určený tým, koľko bajtov ste nakreslili predtým. Dešifrovanie je rovnaká operácia XOR voči rovnakej sekvencii, čo znamená, že výrobca a spotrebiteľ musia odoberať presne rovnaké bajty na presne rovnakých pozíciách.

To je celá obtiaž. Prúdová šifra nemá žiadnu resynchronizáciu. Ak vypltváte jeden bajt keystreamu, každý ďalší bajt bude podrobený operácii XOR voči zlému bajtu keystreamu a chyba sa nikdy sama neopraví; šíri sa kaskádovito až na koniec bloku a keď je bežná pozícia nesprávna, tak aj do každého ďalšieho bloku. Chyba tu urobila presne toto. Počítadlo blokov začínalo od sentinelovej hodnoty mínus jeden a preskakovacia rutina predpokladala, že počítadlo už zodpovedá aktuálnemu bloku. Počnúc týmto sentinelom zmenila kľúč a spustila plný 1024-bajtový blok keystreamu, ktorý sa nemal nikdy spotrebovať, a v procese posunula zostávajúci počet do záporných hodnôt. Od tohto bodu bol dešifrovací program posunutý o celý blok mimo fázu. Overovateľ, kontrolovaný pred týmto všetkým, napriek tomu prešiel, takže heslo vyzeralo správne, zatiaľ čo každá dátová bunka vyšla ako odpad.

Opravená logika žije v TXLSDecrypterRC4. Obe metódy, Skip aj Decrypt, zdieľajú jednu slučku: zmena kľúča prebehne iba vtedy, keď bežná pozícia prejde do nového bloku, pričom index bloku je pozícia vydelená REKEY_BLOCK_SIZE (1024), potom spotrebuje až do zvyšku aktuálneho bloku a nič viac. MakeKey sa volá s indexom bloku, nikdy nie so zastaraným alebo sentinelovým indexom, a pozícia sa posúva o presný počet spracovaných bajtov, takže Skip and Decrypt zostávajú fázovo zladené s producentom. Ponaučenie je v najmenšej jednotke: jediný premrhaný bajt nie je malá chyba v prúdovej šifre, je to úplná strata všetkého nadväzujúceho.

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;

Interop so zmrazenou špecifikáciou znamená zhodu na bajt

Obe chyby sa redukujú na rovnaký základný princíp a stojí za to ho vysloviť samostatne, pretože mení spôsob, akým zvažujete návrhové rozhodnutia. Keď je spotrebiteľom vášho výstupu pevný externý program, ktorý nemôžete zmeniť, režim šifry a kadencia zmeny kľúča nie sú implementačné detaily, ktoré môžete optimalizovať alebo zjednodušiť. Sú súčasťou sieťového kontraktu. Excel bude dešifrovať pomocou ECB a meniť kľúč na hraniciach 1024 bajtov bez ohľadu na to, či sa vám tieto voľby páčia, a vašou jedinou úlohou je produkovať bajty, ktoré sa dešifrujú na originál presne podľa tohto postupu. Režim, ktorý je modernejší, IV, ktorý sa zdá neškodný, počítadlo začínajúce tam, kde to príde prirodzené – čokoľvek z toho je chybou vo chvíli, keď sa to odchýli od toho, čo očakáva čítačka. Interoperabilita proti zmrazenej špecifikácii nie je približná. Je presná na bajt alebo je rozbitá.

This is also why the verifier is a poor smoke test on its own. It tells you the key derivation works, which is necessary but far from sufficient. A test that only opens an encrypted file and confirms the password passes will report success while the body is unreadable. A real test decrypts the package and compares the recovered bytes to the original input, or round-trips a workbook through encrypt and decrypt and reads cells back. The verifier proves the password; only the body proves the encryption.

Podporovaný spôsob čítania a zápisu chránených zošitov

Verejný povrch je malý. Pre zápis heslom chráneného moderného zošita naplňte alebo otvorte TXLSXWorkbook a zavolajte SaveAsEncrypted s názvom súboru a heslom; to serializuje zošit a spustí reťazec Standard Encryption, ktorý prvá oprava uviedla do poriadku, pričom vracia 1 pri úspechu. Pre čítanie zavolajte CanReadEncrypted na otestovanie, či je súbor šifrovaným kontajnerom Compound File, a potom sa vetvite: OpenEncrypted spracováva šifrovanú cestu a vráti sa k Open pre bežné súbory, a Open s heslom je k dispozícii priamo. Spracovanie režimu a slučka zmeny kľúča popísané vyššie sedia pod týmito volaniami; vy dodáte heslo a názov súboru a engine splní špecifikáciu vo vašom mene.

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;

Tvar chráneného výstupu, stream EncryptionInfo, bloky overovateľa a rozloženie balíka sú pokryté v našom návode na výstup XLSX chránený pomocou AES. Pre samostatnú otázku zamykania na úrovni hárka a toho, ako ochrana spolupracuje s nastavením strany a tlačou, si pozrite článok o ochrane, nastavení strany a tlači. Obe témy stavajú na ceste šifrovania popísanej tu, ktorá sa dodáva ako súčasť HotXLS spreadsheet component pre Delphi a C++Builder spolu s rozhraniami API pre čítanie, zápis a vykresľovanie popísanými inde na tomto blogu.