Technický článek

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

Knihovna HotXLS načítá soubory Excel šifrované pomocí Agile — což je ochrana heslem, kterou Excel 2010 a každá novější verze používají ve výchozím nastavení — prostřednictvím jediného volání: TXLSXWorkbook.OpenEncrypted. Komponenta analyzuje XML popisovač šifrování, odvozuje klíče z hesla pomocí řetězce hashů SHA-512 s počtem spinů, ověřuje heslo vůči šifrovanému ověřovateli a poté dešifruje balík v 4096bajtových segmentech AES-CBC. Není do toho zapojena žádná instalace aplikace Excel, žádné COM ani externí kryptografické knihovny DLL

Tento článek se zabývá konkrétně stranou čtení šifrování Agile. Dva související problémy mají své vlastní články: interoperabilita se staršími schématy RC4 a XOR uvnitř starých souborů BIFF .xls je popsána v článku o interoperabilitě ECB a RC4 a vytváření heslem chráněných sešitů se standardním šifrováním ECMA-376 je popsáno v článku o výstupu XLSX chráněném AES. V tomto případě soubor již existuje, někdo jiný jej zašifroval a vaším úkolem je jej otevřít

Scénář, který tento problém vyvolává, zná každý, kdo provozuje dokumentovou linku. Importní služba na straně serveru přijímá nahrávané sešity; na stroji není Excel a nikdy nebude; a jednoho rána zákazník nahraje zcela běžný soubor .xlsx, který čtečka ZIP odmítne, protože to vůbec není ZIP. Zákazník jej uložil s heslem. Od tohoto okamžiku váš zavaděč buď rozumí specifikaci [MS-OFFCRYPTO], nebo vrátí soubor 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 to, co Excel 2010 a novější zapisují, kdykoli je sešit uložen s heslem. Zašifrovaný soubor již není balíčkem ZIP. Jedná se o 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. Podpis CFB (D0 CF 11 E0 A1 B1 1A E1) je stejná značka, jakou nesou starší soubory BIFF .xls, což je důvod, proč přejmenovaný nebo zašifrovaný soubor nelze klasifikovat pouze podle přípony

To, co odlišuje Agile od jeho předchůdců, je to, že EncryptionInfo popisuje sám sebe. Po 8bajtovém prefixu verze, s hlavní i vedlejší verzí 4, je stream deskriptorem XML v kódování UTF-8. Prvek keyData deklaruje šifru (AES), režim řetězení (ChainingModeCBC), hash (SHA512), délku klíče v bitech, velikost bloku a salt v Base64. Prvek hesla keyEncryptor nese vlastní salt, hodnotu spinCount a tři Base64 položky: encryptedVerifierHashInput, encryptedVerifierHashValue a encryptedKeyValue. Excel zapisuje AES-256 s počtem spinů 100 000, ale deskriptor může deklarovat i AES-128 nebo AES-192 a knihovna HotXLS respektuje to, co říká hodnota keyBits, namísto předpokladu 256

Jeden vstupní bod pro sešity s čistým textem, Standard i Agile

Metoda TXLSXWorkbook.OpenEncrypted zpracovává všechny tři stavy, se kterými se může volající setkat — čistý ZIP, Standardně šifrované a Agile šifrované soubory — takže obslužné programy nahrávání nemusí soubory před jejich načtením klasifikovat. Metoda nejprve otestuje soubor: pokud neobsahuje podpis CFB, odkáže na běžnou cestu Open a heslo je jednoduše ignorováno. Pokud je soubor kontejnerem CFB, zkusí nejprve standardní šifrování ECMA-376 a v případě, že verze podpisu EncryptionInfo je Agile 4.4, přesměruje zpracování do linky Agile. Návratová hodnota je 1 při úspěchu, což odpovídá kontraktu metody Open

var
  Wb: TXLSXWorkbook;
begin
  Wb := TXLSXWorkbook.Create;
  try
    // Works for plain .xlsx, Standard-encrypted and
    // Agile-encrypted files alike
    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í řešení pro nešifrované vstupy má větší význam, než se zdá. Dávkový importér, který vždy volá OpenEncrypted, nepotřebuje na místě volání žádné větvení: soubory, které nebyly nikdy chráněny, se načtou přesně jako dříve a soubory, které přicházejí zašifrované, se dešifrují na místě a poté se předají běžnému zavaděči ZIP jako stream v paměti. K testování tak zbývá pouze jedna cesta kódu namísto tří

Jak se z hesla stane klíč AES?

Šifrování Agile nikdy nepoužívá heslo přímo. HotXLS nejprve vypočítá iterovaný hash: výchozí otisk je SHA-512 nad salt hesla zřetězeným s bajty hesla v kódování UTF-16LE a poté se tento otisk znovu zahashuje spinCount-krát, přičemž se v každém kole před předchozí otisk připojí 32bitové počítadlo iterací v little-endian formátu. Při výchozím počtu spinů 100 000 v aplikaci Excel to znamená sto tisíc po sobě jdoucích volání SHA-512 na jeden pokus o heslo, a v tom spočívá celý smysl. Počet spinů slouží jako brzda proti útoku hrubou silou: legitimního volajícího to stojí několik milisekund jednorázově a útočníka se slovníkem stejných několik milisekund na každý jednotlivý odhad

// [MS-OFFCRYPTO] iterated password hash:
//   H(0) = SHA-512(salt + UTF-16LE(password))
//   H(n) = SHA-512(LE32(n - 1) + H(n - 1)), repeated spinCount times
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);            // iteration counter, little-endian
    Move(Result[0], buf[4], 64);   // previous digest
    Result := XlsSHA512(buf);
  end;
end;

Získaný hash po spinech stále není klíčem. Odvozují se z něj tři odlišné klíče jeho dalším zahashováním s připojeným pevným 8bajtovým klíčem bloku, což je jedna konstanta 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í samotného klíče balíku. Každý výsledek SHA-512 je zkrácen na deklarovanou délku klíče a podle specifikace [MS-OFFCRYPTO] doplněn bajty 0x36 v teoretickém případě, kdy je hash kyselší než klíč. Stejné pravidlo doplňování 0x36 platí, když se salt hesla prodlouží na velikost bloku pro použití jako inicializační vektor CBC

Ověření hesla a úskalí zkrácení podle saltSize

Knihovna HotXLS ověřuje heslo dříve, než se dotkne balíku, a to s využitím dvojice verifikátorů z deskriptoru. Dešifruje encryptedVerifierHashInput prvním odvozeným klíčem, zahashuje výsledek pomocí SHA-512, dešifruje encryptedVerifierHashValue druhým odvozeným klíčem a porovná oba otisky bajt po bajtu. Neshoda znamená, že heslo je nesprávné, což se nahlásí jako samostatný výsledek namísto poškozeného sešitu, a co je zásadní, tělo balíku se nikdy nedešifruje špatným klíčem, takže nemůže nastat situace, kdy nesprávné heslo vytvoří věrohodně vypadající poškozená data

To, že verifikátor je saltSize bajtů náhodných dat, kde saltSize je délka saltu šifrátoru klíče, nikoli velikost bloku šifry, je důležitý detail specifikace. Protože je šifra AES-CBC zarovnána 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 velikost saltSize. Excel vždy zapisuje saltSize rovno blockSize, obojí 16, so implementace, která toto zkrácení vynechá, projde všemi testy proti reálnému výstupu z Excelu, ale selže na prvním souboru od tvůrce, který zvolil jinou délku saltu. HotXLS provádí zkrácení na délku saltu, protože to specifikace skutečně vyžaduje, a shoda obou hodnot v praxi je pouze náhodou, nikoli kontraktem

Jak se dešifruje EncryptedPackage?

Stream EncryptedPackage začíná 8bajtovou velikostí čistého textu v little-endian formátu, za níž následují šifrovaná data v 4096bajtových segmentech, a HotXLS je dešifruje segment po segmentu s novým IV pro každý segment. Samotný klíč balíku se neodvozuje z hesla: jedná se o náhodný mezilehlý klíč, který autor zašifroval do encryptedKeyValue, a HotXLS jej rozbalí třetím odvozeným klíčem se zkrácením na délku klíče deklarovanou v keyData. Inicializační vektor (IV) každého segmentu je SHA-512 nad salt z keyData zřetězeným s 32bitovým indexem segmentu v little-endian formátu, zkráceným na velikost bloku. Tato konstrukce znamená, že jakýkoli 4096bajtový segment lze dešifrovat nezávisle, což z principu činí tento formát vhodným pro náhodný přístup, ačkoli HotXLS dešifruje celý balík do paměti a výsledné bajty ZIP předává běžnému zavaděči XLSX

Deklarovaná velikost čistého textu provádí poslední část práce. Výstup AES-CBC je zarovnán na bloky, takže poslední segment nese až 15 bajtů výplně (padding), které nejsou součástí dokumentu; dešifrovaný buffer se zkrátí na velikost z prefixu a výsledkem je přesně ZIP .xlsx, který Excel zašifroval. HotXLS před dešifrováním ověřuje prefix vůči skutečné délce streamu, takže zkrácený nahraný soubor nebo zmanipulované pole velikosti selže čistě namísto přetečení

Hlášení chyb a upřímné limity

Režimy selhání jsou záměrně drženy odděleně. Nesprávné heslo vyvolá výjimku s explicitní zprávou o chybě hesla, která je řízena neshodou verifikátoru, takže uživatelské rozhraní může vyzvat uživatele k opakování pokusu. Kontejner CFB, jehož deskriptor deklaruje algoritmy mimo podporovanou sadu — cokoli jiného než AES s řetězením CBC a hashem SHA-512 v deskriptoru Agile — nebo kontejner, který není Standardní ani Agile, vyvolá jinou výjimku identifikující schéma jako nepodporované. Tyto dvě věci se nesmí nikdy zaměňovat: zkoušení hesla proti nepodporovanému schématu marní čas uživatele a nahlášení chybného hesla jako chyby formátu posílá váš tým podpory nesprávný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
      // Raised for both a wrong password and an unsupported
      // scheme; E.Message states which, so log it verbatim and
      // only offer a password retry for the wrong-password case
      RejectUpload(FileName, E.Message);
  end;
end;

Limity stojí za to uvést na rovinu. HotXLS čte deskriptory Agile, které deklarují AES v režimu CBC se SHA-512, což pokrývá to, co Excel 2010 až Excel 365 reálně zapisují, ve všech třech velikostech klíče. Deskriptory deklarující jiné šifry nebo hashovací algoritmy jsou odmítnuty a šifrátory klíčů založené na certifikátech nejsou dotazovány, pouze šifrátor na základě hesla. Na straně zápisu HotXLS aktuálně produkuje Standardní šifrování namísto Agile, což je rozdíl, na kterém záleží, pokud následné nástroje schéma kontrolují; podrobnosti naleznete v článku o zápisu výstupu XLSX chráněného AES

Password-protected uploads stop being a special case once the loader treats encryption as part of the file format rather than an exception to it. The OpenEncrypted entry point, the SHA-512 spin-count derivation, and the segmented AES-CBC pipeline described here ship as part of the HotXLS Delphi Excel Component, alongside the rest of its native XLS and XLSX reading and writing engine for Delphi and C++Builder