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
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
// 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
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