Műszaki cikk

Agile-titkosítású Excel fájlok olvasása Delphi-ben a HotXLS segítségével

A HotXLS egyetlen hívással olvassa az Agile-titkosítású Excel fájlokat — azt a jelszavas védelmet, amelyet az Excel 2010 és minden későbbi verzió alapértelmezés szerint alkalmaz —: TXLSXWorkbook.OpenEncrypted; a komponens értelmezi az XML titkosítási leírót, SHA-512 spin-count hash-lánccal származtat kulcsokat a jelszóból, ellenőrzi a jelszót a titkosított ellenőrzővel szemben, majd 4096 bájtos AES-CBC szegmensekben dekódolja a csomagot; nincs szükség Excel telepítésére, COM-ra vagy külső kriptográfiai DLL-re

Ez a cikk kifejezetten az Agile titkosítás olvasási oldalával foglalkozik; két kapcsolódó problémának saját cikke van: a régi BIFF .xls fájlokon belüli elavult RC4 és XOR sémák kezelése a szolgáltatások közötti együttműködésről szóló cikkben található, a jelszóval védett munkafüzetek előállítása az ECMA-376 Standard Encryption segítségével pedig a AES-védett XLSX kimenetről szóló cikkben szerepel; itt a fájl már létezik, valaki else titkosította, és az Ön feladata a megnyitása

A problémát kikényszerítő helyzet ismerős bárki számára, aki dokumentum-feldolgozó rendszert üzemeltet; egy szerveroldali importáló szolgáltatás munkafüzet-feltöltéseket fogad; a gépen nincs Excel, és nem is lesz; egy reggel pedig az ügyfél feltölt egy teljesen átlagos .xlsx fájlt, amelyet a ZIP-olvasó elutasít, mert egyáltalalában nem ZIP; az ügyfél jelszóval mentette el; ettől a pillanattól kezdve a betöltőjének vagy értenie kell a [MS-OFFCRYPTO] sémát, vagy vissza kell dobnia a fájlt a felhasználónak, aki az ő szemszögéből semmi szokatlant nem tett

Mi az az Agile titkosítás az Excel fájlban?

Az Agile titkosítás a [MS-OFFCRYPTO] §2.3.4.10-től §2.3.4.15-ig terjedő szakaszában meghatározott jelszóvédelmi séma, és ezt írja az Excel 2010 és a későbbi verziók minden alkalommal, amikor egy munkafüzetet jelszóval mentenek el; a titkosított fájl már nem ZIP csomag; ez egy OLE Compound File Binary (CFB) tároló, amely két adatfolyamot tartalmaz: az EncryptionInfo-t, amely leírja a titkosítás végrehajtását, és az EncryptedPackage-et, ami a tényleges .xlsx ZIP csomag titkosított bájtokként; a CFB aláírás (D0 CF 11 E0 A1 B1 1A E1) megegyezik azzal a jelöléssel, amelyet a régi BIFF .xls fájlok hordoznak, ezért az átnevezett vagy titkosított fájl nem osztályozható pusztán a kiterjesztése alapján

Ami megkülönbözteti az Agile-t az elődeitől, az az, hogy az EncryptionInfo önleíró; egy 8 bájtos verzió-előtag után, amelynél a fő- és alverzió is 4, az adatfolyam egy UTF-8 kódolású XML leíró; a keyData elem deklarálja a rejtjelezőt (AES), a láncolási módot (ChainingModeCBC), a hasht (SHA512), a kulcshosszt bitekben, a blokkméretet és egy Base64 sót; a jelszó keyEncryptor eleme saját sót, a spinCount-ot, és három Base64 terhelést hordoz: encryptedVerifierHashInput, encryptedVerifierHashValue és encryptedKeyValue; az Excel az AES-256-ot alapértelmezetten 100 000-es spin count értékkel írja, de a leíró deklarálhat AES-128-at vagy AES-192-t is, a HotXLS pedig tiszteletben tartja azt, amit a keyBits mond, ahelyett, hogy automatikusan a 256-ot feltételezné

Egyetlen belépési pont a sima, Standard és Agile munkafüzetekhez

A TXLSXWorkbook.OpenEncrypted mindhárom állapotot kezeli, amellyel a hívó találkozhat — a sima ZIP-et, a Standard-titkosítottat és az Agile-titkosítottat —, így a feltöltés-kezelőknek nem kell osztályozniuk a fájlokat a betöltés előtt; a metódus először megvizsgálja a fájlt: ha nincs CFB aláírás, a normál Open útra lép át, és a jelszót egyszerűen figyelmen kívül hagyja; ha a fájl CFB tároló, először az ECMA-376 Standard Encryption-t próbálja meg, és ha az EncryptionInfo verzió-aláírása az Agile 4.4, átirányítja az Agile folyamatra; a visszatérési érték sikeres működés esetén 1, ami megegyezik az Open szerződésével

var
  Wb: TXLSXWorkbook;
begin
  Wb := TXLSXWorkbook.Create;
  try
    // Ugyanúgy működik a sima .xlsx, Standard-titkosított és
    // Agile-titkosítású fájlok esetében is
    if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
      Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
  finally
    Wb.Free;
  end;
end;

A nem titkosított bemenetre való visszalépés fontosabb, mint amilyennek látszik; az a kötegelt importáló, amely mindig az OpenEncrypted-et hívja, nem igényel elágazást a hívás helyén: a soha nem védett fájlok pontosan ugyanúgy töltődnek be, mint korábban, a titkosítottan érkező fájlok pedig a helyükön dekódolódnak, majd in-memory streamként kerülnek a rendes ZIP-olvasóhoz; egyetlen kódútvonalat kell tesztelni, nem hármat

Hogyan válik a jelszó AES kulccsá?

A jelszót az Agile titkosítás soha nem használja közvetlenül; a HotXLS először egy iterált hasht számol ki: a kezdeti kivonat a jelszó-só és a jelszó UTF-16LE bájtainak összefűzésén végrehajtott SHA-512, majd a kivonatot a spinCount-szor újrahasheli, minden körben a 32 bites kis-endian iterációs számlálót illesztve a korábbi kivonat elé; az Excel alapértelmezett 100 000-es spin count értékével ez százezer egymást követő SHA-512 hívást jelent jelszó-kísérletenként, és ez a lényeg; a spin count a nyers erő alapú támadás lassítója: a jogosult hívónak egyszer néhány ezredmásodpercbe kerül, a szótár alapú támadónak pedig ugyanennyibe minden egyes tippnél

// [MS-OFFCRYPTO] iterált jelszó hash:
//   H(0) = SHA-512(salt + UTF-16LE(password))
//   H(n) = SHA-512(LE32(n - 1) + H(n - 1)), spinCount alkalommal ismételve
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);            // iterációs számláló, kis-endian
    Move(Result[0], buf[4], 64);   // előző kivonat
    Result := XlsSHA512(buf);
  end;
end;

Az iterált hash még mindig nem kulcs; három különálló kulcs származik belőle úgy, hogy még egyszer lehashelik egy fix 8 bájtos blokkkulccsal a végén, amely konstans a célnak megfelelően: FE A7 D2 76 3B 4B 9E 79 az ellenőrző bemenet dekódolásához, D7 AA 0F 6D 30 61 34 4E az ellenőrző hash-hez, és 14 6E 0B E7 AB AC D0 D6 a tényleges csomagkulcs felnyitásához; minden SHA-512 eredmény a deklarált kulcshosszra csonkolódik, és a [MS-OFFCRYPTO] szerint 0x36 bájtokkal egészül ki abban az elméleti esetben, ha a hash rövidebb a kulcsnál; ugyanez a 0x36 kiegészítési szabály érvényes, amikor a jelszó-sót blokkméretre terjesztik ki a CBC inicializálási vektorhoz

Jelszó-ellenőrzés és a saltSize csonkolási csapda

A HotXLS a leíróban lévő ellenőrző pár segítségével ellenőrzi a jelszót, mielőtt hozzáérne a csomaghoz; az első származtatott kulccsal dekódolja az encryptedVerifierHashInput-ot, az eredményt SHA-512-vel hasheli, a második származtatott kulccsal dekódolja az encryptedVerifierHashValue-t, és bájtról bájtra összehasonlítja a két kivonatot; az eltérés azt jelenti, hogy a jelszó hibás, ezt különálló eredményként jelenti a sérült munkafüzet helyett, és ami a legfontosabb, a csomagtestet soha nem dekódolja hibás kulccsal, így nincs olyan forgatókönyv, amelyben a hibás jelszó valósnak tűnő, sérült adatokat eredményezne

Van itt egy specifikációs részlet, amelyet könnyű elrontani; az [MS-OFFCRYPTO] §2.3.4.13 az ellenőrzőt saltSize bájtnyi véletlenszerű adatként határozza meg, ahol a saltSize a kulcs-rejtjelező sójának hossza, nem pedig a titkosító blokkmérete; mivel az AES-CBC rejtjelezett szöveg blokkhoz igazodik, a dekódolt ellenőrző bemenet 16 bájt többszörösére kiegészítve érkezik vissza, és a hashelés előtt vissza kell csonkolni a saltSize méretére; az Excel mindig a blokkmérettel megegyező saltSize-t ír, mindkettő 16, így az a megvalósítás, amely kihagyja a csonkolást, átmegy a valódi Excel kimenettel szembeni teszteken, majd elbukik az első olyan előállítótól származó fájlon, amely eltérő sóhosszt választott; a HotXLS a sóhosszra csonkol, mert a specifikáció valójában ezt írja elő, a két érték gyakorlati egyezése pedig véletlen egybeesés, nem pedig szabály

Hogyan történik az EncryptedPackage dekódolása?

Az EncryptedPackage adatfolyam egy 8 bájtos, kis-endian sorrendű nyers szövegmérettel kezdődik, ezt követi a rejtjelezett szöveg 4096 bájtos szegmensekben, a HotXLS pedig szegmensről szegmensre dekódolja azt, szegmensenként friss IV-t használva; maga a csomagkulcs nem jelszóból származik: ez egy véletlenszerű köztes kulcs, amelyet az író az encryptedKeyValue-ba titkosított, a HotXLS pedig a harmadik származtatott kulccsal bontja ki azt, a keyData által deklarált kulcshosszra csonkolva; az egyes szegmensek IV-je a keyData só és a 32 bites kis-endian szegmensindex összefűzésén futtatott SHA-512, a blokkméretre csonkolva; ez a felépítés azt jelenti, hogy bármely 4096 bájtos szegmens önállóan dekódolható, ami elvileg barátságossá teszi a formátumot a közvetlen eléréshez, bár a HotXLS a teljes csomagot a memóriába dekódolja, és a kapott ZIP bájtokat a rendes XLSX olvasójának adja át

A nyers szövegméret-előtag végzi el az utolsó munkát; az AES-CBC kimenet blokkhoz igazodik, így az utolsó szegmens legfeljebb 15 bájtnyi kiegészítést tartalmaz, amely nem része a dokumentumnak; a dekódolt puffer a méret-előtagra csonkolódik, az eredmény pedig pontosan az a .xlsx ZIP lesz, amelyet az Excel titkosított; a HotXLS a dekódolás előtt ellenőrzi az előtagot a tényleges adatfolyam-hosszúsággal szemben, így a csonkolt feltöltés vagy a módosított méretmező tisztán elbukik a túlcsordulás helyett

Hibajelentés és a korrekt határok

A hibaállapotok szándékosan el vannak választva; a hibás jelszó az ellenőrző eltérése miatt kifejezett hibás jelszó üzenettel rendelkező kivételt vált ki, így a felhasználói felület kérheti a felhasználót a próbálkozás megismétlésére; az a CFB tároló, amelynek leírója a támogatott készleten kívüli algoritmusokat deklarál — az Agile leíróban az AES-CBC titkosításon és a SHA-512 hashelésen kívül bármi mást —, vagy az olyan tároló, amely nem Standard és nem Agile, más kivételt vált ki, amely nem támogatottként azonosítja a sémát; a kettőt soha nem szabad összetéveszteni: a jelszó ismételt megadása a nem támogatott sémával szemben csak a felhasználó idejét pazarolja, a hibás jelszó formátumhibaként való jelentése pedig rossz útra tereli a támogatási csapatot

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
      // Kiváltódik hibás jelszó és nem támogatott
      // séma esetén is; az E.Message megadja, melyik, így naplózza szó szerint
      // és csak a hibás jelszó esetén ajánlja fel az újrapróbálkozást
      RejectUpload(FileName, E.Message);
  end;
end;

A határokat érdemes egyértelműen kimondani; a HotXLS beolvassa azokat az Agile leírókat, amelyek az AES-t CBC módban SHA-512-vel deklarálják, ami lefedi azt, amit az Excel 2010 és az Excel 365 valóban ír, mindhárom kulcsméretben; az egyéb rejtjelezőket vagy hash-algoritmusokat deklaráló leírókat elutasítja ahelyett, hogy találgatna, és a tanúsítványalapú kulcs-rejtjelezőkkel sem foglalkozik, csak a jelszavas kulcs-rejtjelezővel; az írási oldalon a HotXLS jelenleg Standard Encryption-t készít az Agile helyett, ami fontos különbség lehet, ha a későbbi eszközök vizsgálják a sémát; a részletek az AES-védett XLSX kimenet írásáról szóló cikkemben találhatók

A jelszóvel védett feltöltések nem számítanak különleges esetnek, amint a betöltő a titkosítást a fájlformátum részeként kezeli, nem pedig annak kivételeként; az OpenEncrypted belépési pont, a SHA-512 spin-count származtatás és az itt leírt szegmentált AES-CBC folyamat a HotXLS Delphi Excel Component részeként érhető el, a natív XLS és XLSX olvasó és író motor többi részével együtt Delphi és C++Builder rendszerekhez