HotXLS číta Excel súbory zašifrované pomocou Agile encryption, teda ochrany heslom, ktorú Excel 2010 a každá neskoršia verzia používajú predvolene, pomocou jediného volania: TXLSXWorkbook.OpenEncrypted. Komponent analyzuje XML deskriptor šifrovania, odvodzuje kľúče z hesla pomocou reťazenej hashovacej funkcie SHA-512 so spin count, overuje heslo voči zašifrovanému overovaciemu reťazcu a následne dešifruje balík po 4096-bajtových segmentoch AES-CBC. Nie je pritom potrebná žiadna inštalácia Excelu, žiadne COM ani žiadna externá kryptografická DLL knižnica
Tento článok sa venuje konkrétne čítacej strane Agile encryption. Dve súvisiace témy majú vlastné články: interoperabilite so staršími schémami RC4 a XOR vo vnútri starých súborov BIFF .xls sa venuje článok o interoperabilite ECB a RC4, a vytváraniu zošitov chránených heslom pomocou ECMA-376 Standard Encryption sa venuje článok o výstupe XLSX chránenom pomocou AES. Tu už súbor existuje, niekto iný ho zašifroval a vašou úlohou je ho otvoriť
Scenár, ktorý túto problematiku vynucuje, pozná každý, kto prevádzkuje pipeline na spracovanie dokumentov. Importná služba na strane servera prijíma nahrané zošity; na danom stroji nie je nainštalovaný Excel a ani nikdy nebude; a jedného rána zákazník nahrá úplne bežný súbor .xlsx, ktorý ZIP čítačka odmietne, pretože vôbec nejde o ZIP. Zákazník ho totiž uložil s heslom. Od toho okamihu buď váš loader rozumie [MS-OFFCRYPTO], alebo súbor vráti späť používateľovi, ktorý z vlastného pohľadu neurobil nič nezvyčajné
Čo je Agile encryption v súbore programu Excel?
Agile encryption je schéma ochrany heslom definovaná v [MS-OFFCRYPTO] §2.3.4.10 až §2.3.4.15 a je to presne to, čo Excel 2010 a novšie verzie zapíšu vždy, keď sa zošit uloží s heslom. Zašifrovaný súbor už nie je balíkom ZIP. Ide o kontajner OLE Compound File Binary (CFB), ktorý obsahuje dva streamy: EncryptionInfo, ktorý popisuje spôsob vykonania šifrovania, a EncryptedPackage, čo je samotný ZIP súboru .xlsx zašifrovaný ako nepriehľadný blob. Signatúra CFB (D0 CF 11 E0 A1 B1 1A E1) je rovnaká magická hodnota, akú nesú aj staršie súbory BIFF .xls, a preto premenovaný alebo zašifrovaný súbor nemožno klasifikovať len podľa prípony
Od svojich predchodcov sa Agile odlišuje tým, že EncryptionInfo je samopopisný. Po 8-bajtovej predpone verzie, kde hlavná aj vedľajšia verzia majú hodnotu 4, nasleduje v streame XML deskriptor v kódovaní UTF-8. Element keyData deklaruje šifru (AES), reťazový režim (ChainingModeCBC), hashovaciu funkciu (SHA512), dĺžku kľúča v bitoch, veľkosť bloku a soľ v Base64. Element keyEncryptor pre heslo nesie vlastnú soľ, hodnotu spinCount a tri hodnoty v Base64: encryptedVerifierHashInput, encryptedVerifierHashValue a encryptedKeyValue. Excel zapisuje AES-256 so spin count 100 000, no deskriptor smie deklarovať aj AES-128 alebo AES-192, a HotXLS rešpektuje presne to, čo uvádza keyBits, namiesto toho, aby predpokladal hodnotu 256
Jeden vstupný bod pre nešifrované zošity a zošity so Standard aj Agile šifrovaním
TXLSXWorkbook.OpenEncrypted ošetruje všetky tri stavy, s ktorými sa volajúci môže stretnúť: obyčajný ZIP, súbor zašifrovaný pomocou Standard Encryption a súbor zašifrovaný pomocou Agile encryption, takže kód spracúvajúci nahrávanie súborov ich pred načítaním nemusí klasifikovať. Metóda súbor najprv preskúma: ak neobsahuje signatúru CFB, odovzdá spracovanie bežnej ceste Open a heslo sa jednoducho ignoruje. Ak je súbor kontajnerom CFB, najprv skúsi ECMA-376 Standard Encryption, a keď signatúra verzie v EncryptionInfo zodpovedá Agile 4.4, presmeruje spracovanie do Agile pipeline. Návratová hodnota je pri úspechu 1, rovnako ako v prípade Open
var
Wb: TXLSXWorkbook;
begin
Wb := TXLSXWorkbook.Create;
try
// Funguje pre bežné súbory .xlsx aj pre súbory šifrované
// pomocou Standard aj Agile encryption
if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
finally
Wb.Free;
end;
end;
Náhradné správanie pre nešifrovaný vstup je dôležitejšie, než sa na prvý pohľad zdá. Dávkový importér, ktorý vždy volá OpenEncrypted, nepotrebuje na mieste volania žiadne vetvenie: súbory, ktoré nikdy neboli chránené, sa načítajú presne ako predtým, a súbory, ktoré prídu zašifrované, sa dešifrujú na mieste a následne sa ako stream v pamäti odovzdajú bežnému ZIP loaderu. Testovať treba jednu cestu kódu, nie tri
Ako sa z hesla stane kľúč AES?
Agile encryption nikdy nepoužíva heslo priamo. HotXLS najprv vypočíta iterovaný hash: počiatočný digest je SHA-512 zo spojenia soli hesla s bajtmi hesla v kódovaní UTF-16LE, a následne sa tento digest prehashuje spinCount-krát, pričom v každom kole sa pred predchádzajúci digest predradí 32-bitové počítadlo iterácie v poradí little-endian. Pri predvolenom spin count Excelu, teda 100 000, ide o sto tisíc sériových volaní SHA-512 na jeden pokus o heslo, a práve v tom je celý zmysel. Spin count funguje ako brzda proti útoku hrubou silou: legitímneho volajúceho stojí len niekoľko milisekúnd raz, zatiaľ čo útočníka so slovníkom stojí rovnakých pár milisekúnd pri každom jednotlivom pokuse
// [MS-OFFCRYPTO] iterovaný hash hesla:
// H(0) = SHA-512(soľ + UTF-16LE(heslo))
// H(n) = SHA-512(LE32(n - 1) + H(n - 1)), opakované 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); // počítadlo iterácie, little-endian
Move(Result[0], buf[4], 64); // predchádzajúci digest
Result := XlsSHA512(buf);
end;
end;
Ani rozvinutý hash ešte nie je kľúčom. Z neho sa odvodia tri odlišné kľúče tak, že sa ešte raz hashuje s pripojeným pevným 8-bajtovým blokovým kľúčom, pričom pre každý účel platí iná konštanta: FE A7 D2 76 3B 4B 9E 79 na dešifrovanie vstupu overovača, D7 AA 0F 6D 30 61 34 4E na hash overovača a 14 6E 0B E7 AB AC D0 D6 na rozbalenie samotného kľúča balíka. Každý výsledok SHA-512 sa skráti na deklarovanú dĺžku kľúča a podľa [MS-OFFCRYPTO] sa v teoretickom prípade, keď je hash kratší ako kľúč, doplní bajtmi 0x36. Rovnaké pravidlo dopĺňania 0x36 platí aj vtedy, keď sa soľ hesla rozširuje na veľkosť bloku, aby sa použila ako inicializačný vektor (IV) pre CBC
Overovanie hesla a nástraha skracovania na saltSize
HotXLS overí heslo skôr, než sa vôbec dotkne balíka, pomocou dvojice overovacích hodnôt z deskriptora. Dešifruje encryptedVerifierHashInput pomocou prvého odvodeného kľúča, výsledok hashuje pomocou SHA-512, dešifruje encryptedVerifierHashValue pomocou druhého odvodeného kľúča a oba digesty porovná bajt po bajte. Nezhoda znamená nesprávne heslo, čo sa nahlási ako samostatný výsledok namiesto poškodeného zošita, a čo je kľúčové, znamená to, že telo balíka sa nikdy nedešifruje nesprávnym kľúčom, takže neexistuje scenár, v ktorom by nesprávne heslo vyprodukovalo vierohodne vyzerajúce poškodené dáta
Tu je jeden detail zo špecifikácie, ktorý sa dá ľahko pokaziť. [MS-OFFCRYPTO] §2.3.4.13 definuje overovač ako saltSize bajtov náhodných dát, pričom saltSize je dĺžka soli šifrovača kľúča, nie veľkosť bloku šifry. Keďže šifrový text AES-CBC je zarovnaný na bloky, dešifrovaný vstup overovača sa vráti doplnený na násobok 16 bajtov a pred hashovaním ho treba skrátiť späť na saltSize. Excel vždy zapisuje saltSize rovné blockSize, obe hodnoty 16, takže implementácia, ktorá toto skracovanie vynechá, prejde každým testom voči skutočnému výstupu Excelu a zlyhá až na prvom súbore od tvorcu, ktorý zvolil inú dĺžku soli. HotXLS skracuje na dĺžku soli preto, lebo presne to hovorí špecifikácia, a to, že sa tieto dve hodnoty v praxi zhodujú, je náhoda, nie záruka
Ako sa dešifruje EncryptedPackage?
Stream EncryptedPackage začína 8-bajtovou veľkosťou nešifrovaných dát v poradí little-endian, po ktorej nasleduje šifrový text v 4096-bajtových segmentoch, a HotXLS ho dešifruje segment po segmente s vlastným IV pre každý segment. Samotný kľúč balíka nie je odvodený z hesla: ide o náhodný medzikľúč, ktorý tvorca zašifroval do encryptedKeyValue, a HotXLS ho rozbaľuje pomocou tretieho odvodeného kľúča, pričom ho skracuje na dĺžku kľúča deklarovanú v keyData. IV každého segmentu je SHA-512 zo spojenia soli z keyData s 32-bitovým indexom segmentu v poradí little-endian, skrátené na veľkosť bloku. Táto konštrukcia znamená, že ľubovoľný 4096-bajtový segment možno dešifrovať nezávisle, čo v princípe robí formát vhodným aj pre náhodný prístup, hoci HotXLS dešifruje celý balík do pamäte a výsledné bajty ZIP odovzdá svojmu bežnému loaderu XLSX
Deklarovaná veľkosť nešifrovaných dát vykoná poslednú časť práce. Výstup AES-CBC je zarovnaný na bloky, takže posledný segment nesie až 15 bajtov výplne, ktoré nie sú súčasťou dokumentu; dešifrovaný buffer sa skráti na hodnotu z predpony s veľkosťou a výsledkom je presne ten ZIP súboru .xlsx, ktorý Excel zašifroval. HotXLS overí predponu voči skutočnej dĺžke streamu ešte pred dešifrovaním, takže nedokončené nahranie alebo pozmenené pole s veľkosťou zlyhá čisto, namiesto toho, aby spôsobilo pretečenie
Hlásenie chýb a poctivé hranice podpory
Chybové stavy sú zámerne oddelené. Nesprávne heslo vyvolá výnimku s explicitnou správou o nesprávnom hesle, ktorú spôsobí nezhoda overovača, takže používateľské rozhranie môže používateľa vyzvať na opätovný pokus. Kontajner CFB, ktorého deskriptor deklaruje algoritmy mimo podporovanej množiny, teda čokoľvek iné než AES s reťazením CBC a hashovaním SHA-512 v Agile deskriptore, alebo kontajner, ktorý nie je ani Standard, ani Agile, vyvolá inú výnimku, ktorá danú schému označí ako nepodporovanú. Tieto dva prípady sa nesmú nikdy zamieňať: opakovanie pokusu s heslom voči nepodporovanej schéme premrhá čas používateľa a nahlásenie nesprávneho hesla ako chyby formátu pošle váš tím podpory po nesprávnej stope
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á sa pri nesprávnom hesle aj pri nepodporovanej
// schéme; E.Message uvádza, o ktorý prípad ide, preto ho
// zaznamenajte doslovne a nový pokus ponúknite len pri hesle
RejectUpload(FileName, E.Message);
end;
end;
Oplatí sa jasne povedať, kde sú hranice. HotXLS číta Agile deskriptory, ktoré deklarujú AES v režime CBC so SHA-512, čo pokrýva presne to, čo Excel 2010 až Excel 365 skutočne zapisujú, vo všetkých troch veľkostiach kľúča. Deskriptory deklarujúce iné šifry alebo hashovacie algoritmy sa odmietajú, namiesto toho, aby sa odhadovali, a šifrovače kľúča založené na certifikátoch sa neberú do úvahy, konzultuje sa iba šifrovač kľúča pre heslo. Na strane zápisu HotXLS v súčasnosti generuje Standard Encryption namiesto Agile, čo je rozdiel, na ktorom záleží, ak nadväzujúce nástroje kontrolujú použitú schému; podrobnosti nájdete v článku o zápise výstupu XLSX chráneného pomocou AES
Nahrávania chránené heslom prestanú byť špeciálnym prípadom vo chvíli, keď loader začne šifrovanie považovať za súčasť formátu súboru, a nie za výnimku z neho. Vstupný bod OpenEncrypted, odvodenie kľúča pomocou SHA-512 so spin count a tu opísaná segmentovaná pipeline AES-CBC sú súčasťou komponentu HotXLS Delphi Excel Component, spolu so zvyškom jeho natívneho enginu na čítanie a zápis XLS a XLSX pre Delphi a C++Builder