Technical Article

Miért utasítja el az Excel a titkosított munkafüzetét: ECB és RC4

Megír egy munkafüzetet, titkosítja egy jelszóval, átadja a fájlt egy kollégájának, és a kolléga megnyitja azt az Excelben. Az Excel kéri a jelszót. A kolléga beírja, az Excel pedig elfogadja azt. Eddig a titkosítás helyesnek tűnik. Ezután az Excel feldob egy párbeszédpanelt, amely szerint a fájl sérült és nem nyitható meg, vagy megnyílik értelmetlen cellákkal teli munkalappal. A jelszó helyes volt. A fájl mégis hibás. Ez az Office-titkosítás leginkább zavarba ejtő hibamódja, mert a jelszót ellenőrző rész és az adatokat tartalmazó rész két különböző művelettel van védve, és az egyik helyessége semmit sem garantál a másikra nézve.

Mindkét itt leírt hiba pontosan ilyen felépítésű volt. Mindkét esetben az ellenőrző elfogadta a jelszót, de a törzs nem, ami arra készteti Önt, hogy jelszó- vagy kulcsszármaztatási hibát keressen, ami nincs is ott. A valós hiba downstream volt, abban, hogyan alakították át a csomag bájtokat. A két hiba független, az egyik az AES, a másik az RC4 útvonalon jelentkezik, de közös diagnosztikai problémán osztoznak, ezért érdemes megnézni, miért a félig helyes eredmény a legnehezebben értelmezhető.

Miért nem bizonyít semmit a sikeres jelszó a törzsről

A modern titkosított XLSX által használt formátum az ECMA-376 szabványos titkosítás (Standard Encryption), és két titkosított dolgot tárol egymás mellett. Az egyik az EncryptionVerifier: egy kis blokk, amely egy véletlenszerű értéket és az érték hash-ét tartalmazza, a jelszóból származtatott kulccsal titkosítva. A másik az EncryptedPackage: a munkafüzet teljes zip-konténere, ugyanazzal a kulccsal titkosítva. Az ellenőrző azért létezik, hogy az olvasó megerősíthesse a jelszót, mielőtt energiát fordítana a törzs megabájtjaira. Fejtse vissza az ellenőrzőt, hashelje a véletlenszerű értéket, hasonlítsa össze a tárolt hash-sel, és ha megegyeznek, a jelszó helyes.

A csapda az, hogy az ellenőrző és a csomag titkosítása különálló hívásokkal történik különálló puffereken. A helyesen származtatott kulcs helyesen fejt ki vissza az ellenőrzőt, függetlenül attól, hogy mi történik a csomaggal később. Tehát ha a kulcsszármaztatása helyes, de a csomag transzformációja hibás, az Excel megerősíti a jelszót az ellenőrzőből, majd meghiúsul a törzsnél. A tünet így olvasható: "helyes jelszó, sérült fájl", ami a vizsgálatot a jelszó útvonalára irányítja, ami az egyetlen rész, amely soha nem volt hibás. Ugyanez a megosztás irányítja az örökölt RC4 esetet is: a hitelesítő hash először ellenőrződik, és a szinkronból kieső törzs még mindig érintetlenül hagyja ezt az ellenőrzést.

Első hiba: AES az ECB-ben, nem a CBC-ben

A [MS-OFFCRYPTO] §2.3.4.15 előírja, hogy a Standard Encryption a csomagot AES-sel Electronic Codebook módban titkosítja. A kitöltött csomag minden 16 bájtos blokkját függetlenül titkosítja ugyanazzal a kulccsal. A blokkok között nincs láncolás, és nincs inicializáló vektor. Ez szokatlan választás a modern szabványok szerint, ahol az ECB-t általában kerülik, de az átjárhatóság nem az a hely, ahol felülbíráljuk a specifikációt. Az Excel ECB-ként fejt vissza a csomagot, így a gyártónak ECB-ként kell titkosítania azt, különben nem fognak egyezni.

A hiba az volt, hogy a csomagot AES-sel CBC módban titkosították, csupa nulla inicializáló vektorral. Ezért működik ez majdnem, és a majdnem a legrosszabb hely, ahol landolhatunk. CBC-ben az első nyílt szöveges blokk XOR-olódik az IV-vel a titkosítás előtt. Ha az IV csupa nulla, az a XOR semmit sem változtat, így a CBC-with-zero-IV első blokkja pontosan megegyező titkosított szöveget eredményez, mint az ECB. A második blokktól kezdve a CBC az előző titkosított blokkot táplálja a következőbe, így a második blokktól kezdve minden blokk eltér az ECB-től.

Most helyezze ezt rá a struktúrára. A csomag elrendezése egy 8 bájtos little-endian hosszúságú előtagot helyez el a legeslegelején, így a fájl azon részei, amelyeket az Excel a legkorábban ellenőriz, az első egy vagy két blokkban ülnek. A véletlenül egyező első blokk azt jelenti, hogy a legkorábbi validáció sikeres, miközben minden későbbi blokk zajra fejtődik vissza. A javítás nem bonyolult, amint a módot elnevezték: titkosítsa az egyes 16 bájtos blokkokat ECB-vel, és állítsa le a láncolást. A motorban a XlsEncryptStdPackage végigmegy a kitöltött pufferen 16 bájtos lépésekben, és meghívja az AESEncryptECB128Block-ot mindegyikre, ami ugyanaz a primitív, mint amelyet az ellenőrző blokkokhoz már használunk. A forrás tartalmaz egy megjegyzést a ciklusnál, amely egyértelműen kimondja a szabályt: a nulla IV-vel rendelkező CBC csak az első blokkban egyezik meg az ECB-vel, így a csomag többi része szemetet eredményezne, és az Excel elutasítaná azt.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted serializes the workbook, then runs the
    // ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
    // package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Második hiba: az RC4 kulcsújraosztás elcsúszik a ritmusból

Az örökölt .xls útvonal az RC4 CryptoAPI sémát használja, szabálya pedig más jellegű. A [MS-OFFCRYPTO] §2.3.6 előírja, hogy a rejtjelezőt minden 1024 bájtos blokkhatárnál újra kell kulcsolni. Az adatfolyam 1024 bájtos blokkokra van osztva, friss RC4 kulcs származik a 0., 1., 2. és így tovább blokkszámokhoz, és mindegyik blokkon belül a kulcsfolyamot folyamatosan fogyasztjuk bájtról bájtra. Két invariánsnak kell együtt érvényesülnie: újra-kulcsolás minden határon, és a kulcsfolyam megszakítás nélküli fogyasztása a blokkon belül. Az RC4 egy folyam-rejtjelező (stream cipher), így a kulcsfolyama egyetlen rendezett szekvencia; az n-edik lehúzott bájtot az határozza meg, hogy hány bájtot húzott le előtte. A visszafejtés ugyanaz a XOR ugyanezen szekvencia ellen, ami azt jelenti, hogy az előállítónak és a fogyasztónak pontosan ugyanazokat a bájtokat kell lehúznia pontosan ugyanazokon a pozíciókon.

Ez a teljes nehézség. A folyam-rejtjelezőnek nincs újraszinkronizálása. Ha elveszít egyetlen bájtot a kulcsfolyamból, a mögötte lévő minden egyes bájt a rossz kulcsfolyam-bájttal lesz XOR-olva, és a hiba soha nem javítja ki magát; lefut a blokk végéig, és a futó pozíció elrontásával minden utána következő blokkig terjed. A hiba itt pontosan ezt tette. A blokkszámláló a mínusz egy sentinel értéktől indult, és a kihagyó rutin (skip routine) azt feltételezte, hogy a számláló már egyezik az aktuális blokkhoz. Ebből a sentinelből kiindulva újra-kulcsolt és lefuttatott egy teljes 1024 bájtos kulcsfolyam-blokkot, amelyet soha nem lett volna szabad elfogyasztani, és közben a hátralévő számot negatívba hajtotta. Ettől a ponttól kezdve a visszafejtő egy teljes blokk fáziskésésben volt. A hitelesítő (verifier) – amelyet mindezek előtt ellenőriztek – továbbra is átment, így a jelszó helyesnek tűnt, miközben minden adatcella szemétként jött vissza.

A javított logikát a TXLSDecrypterRC4 hordozza. A Skip és Decrypt metódusok egyaránt ugyanazon a cikluson osztoznak: újra-kulcsolás csak akkor történik, ha a futó pozíció átlép egy új blokkba, ahol a blokkindex a pozíció osztva a REKEY_BLOCK_SIZE (1024) értékkel, majd az aktuális blokk hátralévő részéig fogyasztunk, és nem tovább. A MakeKey-t a blokkindexszel hívjuk meg, soha nem elavult vagy sentinel indexszel, a pozíció pedig pontosan a feldolgozott bájtok számával halad előre, így a Skip és a Decrypt fázisban maradnak az előállítóval. A tanulság a legkisebb egységben rejlik: egyetlen elveszett bájt nem kis hiba a folyam-rejtjelezőben, hanem a teljes tartalom elvesztése downstream.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    // CanReadEncrypted checks the Compound File (OLE2) signature so
    // you can branch before attempting a normal Open. OpenEncrypted
    // routes plain files to Open and handles the encrypted container.
    if Book.CanReadEncrypted('legacy.xls') then
      Book.OpenEncrypted('legacy.xls', 'S3cret!')
    else
      Book.Open('legacy.xls');
    // read cells here
  finally
    Book.Free;
  end;
end;

Az átjárhatóság egy rögzített specifikációval bájtra pontos egyezést jelent

Mindkét hiba ugyanarra az alapelvre vezethető vissza, és ezt érdemes önmagában is kijelenteni, mert megváltoztatja, hogyan mérlegeli a tervezési döntéseket. Amikor a kimenet fogyasztója egy rögzített külső program, amelyet nem tud megváltoztatni, a rejtjelező mód és az újra-kulcsolási ritmus nem olyan implementációs részletek, amelyeket optimalizálhat vagy egyszerűsíthet. Ezek a vezetékes szerződés részei. Az Excel az ECB-vel fog visszafejteni és 1024 bájtos határokon fog újra-kulcsolni, akár tetszik Önnek ez a választás, akár nem, és az Ön egyetlen feladata olyan bájtok előállítása, amelyek ezen pontos eljárás szerint visszafejtődnek az eredetire. Egy korszerűbb mód, egy ártalmatlannak tűnő IV, egy természetes helyről induló számláló; ezek bármelyike hiba abban a pillanatban, amikor eltér attól, amit az olvasó elvár. Az átjárhatóság egy rögzített specifikáció ellenében nem megközelítő jellegű. Bájt-pontos vagy törött.

Ez az oka annak is, hogy az ellenőrző önmagában rossz füstteszt. Azt bizonyítja, hogy a kulcsszármaztatás működik, ami szükséges, de messze nem elégséges. Az a teszt, amely csak megnyitja a titkosított fájlt és megerősíti a jelszót, sikert fog jelenteni, miközben a törzs olvashatatlan. Egy valós teszt visszafejti a csomagot és összehasonlítja a visszanyert bájtokat az eredeti bemenettel, vagy oda-vissza futtat egy munkafüzetet a titkosításon és visszafejtésen, és visszaolvassa a cellákat. Az ellenőrző a jelszót bizonyítja; csak a törzs bizonyítja a titkosítást.

A védett munkafüzetek olvasásának és írásának támogatott módja

A nyilvános felület kicsi. Egy password-protected modern munkafüzet írásához töltse fel vagy nyissa meg a TXLSXWorkbook-ot, és hívja meg a SaveAsEncrypted-et a fájlnévvel és jelszóval; ez szerializálja a munkafüzetet és lefuttatja a Standard Encryption csővezetéket, amelyet az első javítás korrigált, 1 értéket adva vissza siker esetén. Az olvasáshoz hívja meg a CanReadEncrypted-et annak ellenőrzésére, hogy a fájl titkosított Compound File konténer-e, majd ágazzon el: a OpenEncrypted kezeli a titkosított útvonalat, és visszalép az Open-re a sima fájloknál, az Open pedig közvetlenül elérhető jelszóval. A mód kezelése és az újra-kulcsolási ciklus ezen hívások alatt helyezkedik el; Ön megadja a jelszót és a fájlnevet, a motor pedig végrehajtja a specifikációt az Ön nevében.

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // Reopen on the consumer side
    Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
  finally
    Book.Free;
  end;
end;

The shape of the protected output, the EncryptionInfo stream, the verifier blocks, and the package layout are covered in our walkthrough of AES-protected XLSX output. For the separate question of sheet-level locking and how protection interacts with page setup and printing, see the article on protection, page setup, and printing. Both build on the encryption path described here, which ships as part of the HotXLS spreadsheet component for Delphi and C++Builder alongside the reading, writing, and rendering APIs covered elsewhere on this blog.