Technický článek

Čtení souborů Excel šifrovaných Agile v Delphi s HotXLS

HotXLS čte soubory Excel se šifrováním Agile, tedy ochranu heslem, kterou Excel 2010 a všechny pozdější verze používají ve výchozím nastavení, jediným voláním: TXLSXWorkbook.OpenEncrypted. Komponenta rozparsuje XML deskriptor šifrování, odvodí z hesla klíče řetězcem hashů SHA-512 s daným spin count, ověří heslo proti zašifrovanému verifikátoru a poté dešifruje balíček v segmentech AES-CBC o 4096 bajtech. Není k tomu potřeba instalace Excelu, COM ani externí kryptografická DLL

Tento článek se věnuje konkrétně straně čtení šifrování Agile. Dva sousední problémy mají vlastní články: interoperabilitu se staršími schématy RC4 a XOR uvnitř starých souborů BIFF .xls popisuje článek o interoperabilitě ECB a RC4 a vytváření sešitů chráněných heslem pomocí ECMA-376 Standard Encryption popisuje článek o výstupu XLSX chráněného AES. Zde soubor již existuje, zašifroval jej někdo jiný a vaším úkolem je jej otevřít

Scénář, který tento problém vynutí, zná každý, kdo provozuje dokumentovou pipeline. Serverová importní služba přijímá nahrané sešity; na stroji není Excel a nikdy nebude; a jednoho rána zákazník nahraje zcela obyčejný .xlsx, který čtečka ZIP odmítne, protože to vůbec není ZIP. Zákazník jej uložil s heslem. Od té chvíle váš loader buď rozumí [MS-OFFCRYPTO], nebo soubor vrací uživateli, který ze svého pohledu neudělal nic neobvyklého

Co je šifrování Agile v souboru Excel?

Šifrování Agile je schéma ochrany heslem definované v [MS-OFFCRYPTO] §2.3.4.10 až §2.3.4.15 a je to přesně to, co Excel 2010 a novější zapisují, kdykoli je sešit uložen s heslem. Zašifrovaný soubor už není balíček ZIP. Je to kontejner OLE Compound File Binary (CFB) obsahující dva streamy: EncryptionInfo, který popisuje, jak bylo šifrování provedeno, a EncryptedPackage, což je skutečný ZIP .xlsx zašifrovaný jako neprůhledný blob. Signatura CFB (D0 CF 11 E0 A1 B1 1A E1) je táž magická hodnota, jakou nesou staré soubory BIFF .xls, a proto přejmenovaný nebo zašifrovaný soubor nelze klasifikovat jen podle přípony

Anatomie sešitu se šifrováním Agile, který HotXLS otevírá v Delphi: kontejner CFB obsahující stream deskriptoru EncryptionInfo a stream EncryptedPackage ze segmentů AES-CBC
HotXLS čte sešit se šifrováním Agile jako kontejner CFB se samopopisným streamem EncryptionInfo a ZIP .xlsx zašifrovaným jako neprůhledný stream EncryptedPackage

Agile se od svých předchůdců liší tím, že EncryptionInfo je samopopisný. Po 8bajtové předponě verze, kde hlavní i vedlejší verze jsou 4, je stream XML deskriptorem v UTF-8. Prvek keyData deklaruje šifru (AES), režim řetězení (ChainingModeCBC), hash (SHA512), délku klíče v bitech, velikost bloku a sůl v Base64. Prvek keyEncryptor pro heslo nese vlastní sůl, spinCount a tři payloady v Base64: encryptedVerifierHashInput, encryptedVerifierHashValue a encryptedKeyValue. Excel zapisuje AES-256 se spin count 100 000, ale deskriptor smí deklarovat AES-128 nebo AES-192 a HotXLS respektuje to, co říká keyBits, místo aby předpokládal 256

Jeden vstupní bod pro nešifrované sešity i sešity Standard a Agile

TXLSXWorkbook.OpenEncrypted zvládá všechny tři stavy, na které může volající narazit, prostý ZIP, šifrování Standard a šifrování Agile, takže obslužné rutiny nahrávání nemusí soubory před načtením klasifikovat. Metoda soubor nejprve očichá: pokud signatura CFB chybí, přenechá práci běžné cestě Open a heslo je jednoduše ignorováno. Pokud je soubor kontejner CFB, zkusí nejprve ECMA-376 Standard Encryption, a když signatura verze EncryptionInfo odpovídá Agile 4.4, předá řízení pipeline Agile. Návratová hodnota je 1 při úspěchu, stejný kontrakt jako u Open

var
  Wb: TXLSXWorkbook;
begin
  Wb := TXLSXWorkbook.Create;
  try
    // Funguje stejně pro prosté .xlsx, soubory se šifrováním
    // Standard i soubory se šifrováním Agile
    if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
      Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
  finally
    Wb.Free;
  end;
end;

Záložní cesta pro nešifrovaný vstup je důležitější, než vypadá. Dávkový importér, který vždy volá OpenEncrypted, nepotřebuje v místě volání žádné větvení: soubory, které nikdy chráněny nebyly, se načtou přesně jako dříve, a soubory, které dorazí zašifrované, se dešifrují na místě a poté předají běžnému loaderu ZIP jako stream v paměti. Testuje se jedna cesta kódu, ne tři

Jak se z hesla stane klíč AES?

Šifrování Agile nikdy nepoužívá heslo přímo. HotXLS nejprve spočítá iterovaný hash: počáteční digest je SHA-512 ze soli hesla zřetězené s bajty hesla v UTF-16LE a tento digest se poté spinCount krát znovu hashuje, přičemž každé kolo předřadí před předchozí digest 32bitový čítač iterací v little-endian. S výchozím spin count Excelu 100 000 to je sto tisíc sériových volání SHA-512 na jeden pokus o heslo, a přesně o to jde. Spin count je brzda proti hrubé síle: legitimního volajícího stojí jednou několik milisekund a slovníkového útočníka stojí tytéž milisekundy za každý jednotlivý pokus

Jak HotXLS odvozuje klíče šifrování Agile v Delphi: heslo projde 100 000 koly SHA-512 a poté tři pevné 8bajtové blokové klíče dávají klíče verifikátoru a balíčku
Heslo nikdy nic nedešifruje přímo: řetězec SHA-512 se spin count napájí tři hashe s blokovými klíči, které vytvoří klíče verifikátoru a rozbalí náhodný klíč balíčku
// Iterovaný hash hesla podle [MS-OFFCRYPTO]:
//   H(0) = SHA-512(salt + UTF-16LE(password))
//   H(n) = SHA-512(LE32(n - 1) + H(n - 1)), opakováno spinCount krát
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);            // čítač iterací, little-endian
    Move(Result[0], buf[4], 64);   // předchozí digest
    Result := XlsSHA512(buf);
  end;
end;

Ani hash po iteracích ještě není klíč. Odvozují se z něj tři různé klíče tak, že se ještě jednou hashuje s připojeným pevným 8bajtovým blokovým klíčem, jednou konstantou pro každý účel: FE A7 D2 76 3B 4B 9E 79 pro dešifrování vstupu verifikátoru, D7 AA 0F 6D 30 61 34 4E pro hash verifikátoru a 14 6E 0B E7 AB AC D0 D6 pro rozbalení skutečného klíče balíčku. Každý výsledek SHA-512 se zkrátí na deklarovanou délku klíče a podle [MS-OFFCRYPTO] se v teoretickém případě, kdy je hash kratší než klíč, doplní bajty 0x36. Totéž pravidlo doplnění 0x36 platí, když se sůl hesla rozšiřuje na velikost bloku pro použití jako inicializační vektor CBC

Ověření hesla a past zkrácení na saltSize

HotXLS ověří heslo dříve, než se dotkne balíčku, pomocí dvojice verifikátorů z deskriptoru. Dešifruje encryptedVerifierHashInput prvním odvozeným klíčem, výsledek zahashuje SHA-512, dešifruje encryptedVerifierHashValue druhým odvozeným klíčem a oba digesty porovná bajt po bajtu. Neshoda znamená špatné heslo, hlášené jako samostatný výsledek, a ne jako zkomolený sešit, a hlavně znamená, že tělo balíčku se nikdy nedešifruje špatným klíčem, takže neexistuje scénář, kdy by špatné heslo vytvořilo věrohodně vypadající poškozená data

Je tu detail specifikace, který se dá snadno splést. [MS-OFFCRYPTO] §2.3.4.13 definuje verifikátor jako saltSize bajtů náhodných dat, kde saltSize je délka soli key encryptoru, nikoli velikost bloku šifry. Protože šifrový text AES-CBC je zarovnaný na bloky, dešifrovaný vstup verifikátoru se vrací doplněný na násobek 16 bajtů a před hashováním musí být zkrácen zpět na saltSize. Excel vždy zapisuje saltSize rovný blockSize, obojí 16, takže implementace, která zkrácení vynechá, projde každým testem proti skutečnému výstupu Excelu a pak selže na prvním souboru od producenta, který zvolil jinou délku soli. HotXLS zkracuje na délku soli, protože to specifikace skutečně říká, a shoda obou hodnot v praxi je náhoda, ne kontrakt

Jak se dešifruje EncryptedPackage?

Stream EncryptedPackage začíná 8bajtovou velikostí otevřeného textu v little-endian, po níž následuje šifrový text v segmentech po 4096 bajtech, a HotXLS jej dešifruje segment po segmentu s novým IV pro každý segment. Samotný klíč balíčku není odvozen z hesla: je to náhodný mezilehlý klíč, který zapisovatel zašifroval do encryptedKeyValue, a HotXLS jej rozbalí třetím odvozeným klíčem, zkráceným na délku klíče deklarovanou v keyData. IV každého segmentu je SHA-512 ze soli keyData zřetězené s 32bitovým indexem segmentu v little-endian, zkrácený na velikost bloku. Tato konstrukce znamená, že libovolný segment o 4096 bajtech lze dešifrovat nezávisle, což formát v principu činí přátelským k náhodnému přístupu, ačkoli HotXLS dešifruje celý balíček do paměti a výsledné bajty ZIP předá svému běžnému loaderu XLSX

HotXLS dešifruje stream EncryptedPackage v Delphi segment po segmentu a IV každého 4096bajtového segmentu AES-CBC odvozuje ze soli keyData a indexu segmentu
Každý 4096bajtový segment AES-CBC se dešifruje nezávisle pod vlastním IV ze soli a indexu a výsledek se zkrátí na deklarovanou velikost otevřeného textu

Poslední kus práce odvede deklarovaná velikost otevřeného textu. Výstup AES-CBC je zarovnaný na bloky, takže poslední segment nese až 15 bajtů výplně, které nejsou součástí dokumentu; dešifrovaný buffer se zkrátí na velikost z předpony a výsledkem je přesně ten ZIP .xlsx, který Excel zašifroval. HotXLS předponu před dešifrováním ověří proti skutečné délce streamu, takže useknutý upload nebo zmanipulované pole velikosti selže čistě, místo aby došlo k přetečení

Hlášení chyb a poctivé hranice

Režimy selhání jsou záměrně drženy odděleně. Špatné heslo vyvolá výjimku s explicitní zprávou o špatném heslu, řízenou neshodou verifikátoru, takže UI může uživatele vyzvat k novému pokusu. Kontejner CFB, jehož deskriptor deklaruje algoritmy mimo podporovanou množinu, cokoli jiného než AES s řetězením CBC a hashováním SHA-512 v deskriptoru Agile, nebo kontejner, který není ani Standard, ani Agile, vyvolá jinou výjimku označující schéma jako nepodporované. Tyto dva případy se nikdy nesmí slít: opakování hesla proti nepodporovanému schématu plýtvá časem uživatele a hlášení špatného hesla jako chyby formátu pošle váš tým podpory špatným směrem

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
      // Vyvolána jak při špatném heslu, tak při nepodporovaném
      // schématu; E.Message uvádí, o který případ jde, proto ji zalogujte
      // doslovně a nový pokus o heslo nabídněte jen u špatného hesla
      RejectUpload(FileName, E.Message);
  end;
end;

Hranice stojí za to vyslovit jasně. HotXLS čte deskriptory Agile, které deklarují AES v režimu CBC se SHA-512, což pokrývá to, co Excel 2010 až Excel 365 skutečně zapisují, ve všech třech velikostech klíče. Deskriptory deklarující jiné šifry nebo hashovací algoritmy jsou odmítnuty, místo aby se hádaly, a key encryptory založené na certifikátech se nekonzultují, pouze key encryptor pro heslo. Na straně zápisu HotXLS v současnosti vytváří Standard Encryption, nikoli Agile, což je rozdíl, na kterém záleží, pokud navazující nástroje schéma kontrolují; podrobnosti najdete v článku o zápisu výstupu XLSX chráněného AES

Nahrané soubory chráněné heslem přestanou být zvláštním případem, jakmile loader bere šifrování jako součást formátu souboru, a ne jako výjimku z něj. Vstupní bod OpenEncrypted, odvozování SHA-512 se spin count a segmentovaná pipeline AES-CBC popsané zde jsou součástí komponenty HotXLS pro Excel v Delphi, vedle zbytku jejího nativního enginu pro čtení a zápis XLS a XLSX pro Delphi a C++Builder