Jūs įrašote darbaknygę, užšifruojate ją slaptažodžiu, perduodate failą kolegai ir kolega jį atidaro Excel programoje. Excel paprašo slaptažodžio. Kolega jį įveda ir Excel jį priima. Kol kas šifravimas atrodo teisingas. Tada Excel parodo pranešimą, kad failas yra sugadintas ir jo negalima atidaryti, arba atidaro jį su beprasmiais langeliais. Slaptažodis buvo teisingas. Failas vis tiek neveikia. Tai labiausiai klaidinanti klaida Office šifravime, nes slaptažodį tikrinanti dalis ir jūsų duomenis sauganti dalis yra apsaugotos dviem skirtingomis operacijomis, o teisingas vienos atlikimas negarantuoja kitos sėkmės.
Abiem čia aprašytomis klaidomis buvo būtent ši forma. Kiekvienu atveju verifikatorius praėjo, o kūnas (body) – ne, kas verčia ieškoti slaptažodžio gavimo klaidos, kurios ten nėra. Tikroji problema buvo žemiau, tame, kaip buvo transformuojami paketo baitai. Šios dvi klaidos yra nepriklausomos, viena AES kelyje, kita RC4 kelyje, tačiau jas vienija bendra diagnozavimo problema, todėl verta suprasti, kodėl pusiau teisingas rezultatas yra sunkiausiai išanalizuojamas.
Kodėl teisingas slaptažodis nieko nepasako apie failo kūną
Formatas, kurį naudoja šiuolaikinė užšifruota XLSX darbaknygė, yra ECMA-376 standartinis šifravimas (Standard Encryption), ir jis saugo du užšifruotus dalykus šalia vienas kito. Vienas iš jų yra EncryptionVerifier – mažas blokas, turintis atsitiktinę reikšmę ir tos reikšmės maišos (hash) kodą, užšifruotą iš slaptažodžio gautu raktu. Kitas yra EncryptedPackage – visas darbaknygės zip konteineris, užšifruotas tuo pačiu raktu. Verifikatorius reikalingas tam, kad skaitytuvas galėtų patikrinti slaptažodį prieš pradedant dešifruoti megabaitus failo kūno. Dešifruokite verifikatorių, apskaičiuokite atsitiktinės reikšmės maišą, palyginkite ją su išsaugota ir, jei jos sutampa, slaptažodis yra teisingas.
Spąstai yra tai, kad verifikatorius ir paketas šifruojami atskirais iškvietimais skirtinguose buferiuose. Teisingai gautas raktas sėkmingai dešifruos verifikatorių nepriklausomai nuo to, kas nutiko paketui vėliau. Taigi, jei jūsų rakto gavimas yra teisingas, bet paketo transformacija klaidinga, Excel patvirtins slaptažodį iš verifikatoriaus ir tada suges dešifruodama kūną. Simptomas atrodo kaip „teisingas slaptažodis, sugadintas failas“, kas nukreipia tyrimą į slaptažodžio gavimo kelią, kuris kaip tik veikė teisingai. Tas pats atskyrimas galioja ir senajai RC4 sistemai: verifikatoriaus maiša patikrinama pirma, o kūnas, kuris nukrypo, vis tiek leidžia šiai patikrai praeiti sėkmingai.
Pirmoji klaida: AES veikė ECB režimu, o ne CBC
[MS-OFFCRYPTO] §2.3.4.15 nurodo, kad standartinis šifravimas (Standard Encryption) užšifruoja paketą naudodamas AES Electronic Codebook (ECB) režimu. Kiekvienas 16 baitų paketo blokas šifruojamas atskirai tuo pačiu raktu. Nėra jokio grandininio ryšio tarp blokų (chaining) ir nenaudojamas inicializacijos vektorius. Tai neįprastas pasirinkimas pagal šiuolaikinius standartus, kur ECB paprastai vengiama, tačiau suderinamumo (interop) klausimuose specifikacijos negalima keisti. Excel dešifruoja paketą kaip ECB, todėl rašymo programa privalo jį užšifruoti kaip ECB, kad jie sutaptų.
Klaida buvo ta, kad paketas buvo šifruojamas AES CBC režimu, naudojant nulinį inicializacijos vektorių. Štai kodėl tai beveik veikia, ir kodėl „beveik“ yra blogiausia vieta. CBC režime pirmasis teksto blokas yra XORinamas su IV prieš šifravimą. Kai IV yra visi nuliai, šis XOR nieko nekeičia, todėl pirmasis CBC su nuliniu IV blokas sugeneruoja tiksliai tą patį šifruotą tekstą kaip ir ECB režimas. Nuo antrojo bloko CBC perduoda praėjusį šifruotą tekstą kitam blokui, todėl visi vėlesni blokai skiriasi nuo ECB.
Dabar pritaikykite tai struktūrai. Paketo išdėstymas pačioje pradžioje turi 8 baitų ilgio prefiksą, todėl dalys, kurias Excel tikrina anksčiausiai, yra pirmame ar antrame bloke. Pirmasis blokas, kuris sutampa, leidžia ankstyvajai patikrai praeiti sėkmingai, o visi vėlesni blokai dešifruojami į triukšmą. Pataisymas yra aiškus: šifruoti kiekvieną 16 baitų bloką ECB režimu ir nenaudoti grandininio ryšio. Variklyje XlsEncryptStdPackage eina per buferį 16 baitų žingsniais ir iškviečia AESEncryptECB128Block kiekvienam iš jų. Kodas turi komentarą, aiškiai nurodantį, kad CBC su nuliniu IV atitinka ECB tik pirmam blokui, todėl likusi paketo dalis dešifruotųsi į šiukšles ir Excel ją atmestų.
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;
Antroji klaida: RC4 rakto keitimas nukrypo nuo fazės
Senasis .xls kelias naudoja RC4 CryptoAPI schemą, ir jos taisyklė yra kitokia. [MS-OFFCRYPTO] §2.3.6 nurodo, kad šifras yra iš naujo sugeneruojamas (re-keyed) ties kiekviena 1024 baitų bloko riba. Srautas padalijamas į 1024 baitų blokus, naujas RC4 raktas gaunamas blokams 0, 1, 2 ir t. t., o kiekviename bloke raktų srautas (keystream) naudojamas nepertraukiamai. Turi galioti dvi taisyklės: keisti raktą ties kiekviena riba ir naudoti raktų srautą be tarpų bloko viduje. RC4 yra srauto šifras, todėl jo raktų srautas yra viena tvarkinga seka – n-tasis baitas, kurį paimate, priklauso nuo to, kiek baitų paėmėte prieš tai. Dešifravimas yra tas pats XOR prieš tą pačią seką, kas reiškia, kad gamintojas ir vartotojas turi paimti tuos pačius baitus tose pačiose vietose.
Tai ir yra visas sudėtingumas. Srauto šifras neturi resinchonizacijos. Jei prarasite bent vieną raktų srauto baitą, kiekvienas sekantis baitas bus XORinamas su neteisingu raktų srauto baitui, ir klaida niekada nebus ištaisyta – ji plis iki bloko pabaigos ir į kiekvieną sekantį bloką. Ši klaida darė būtent tai. Blokų skaitiklis prasidėdavo nuo neigiamos reikšmės, o praleidimo (skip) rutina manė, kad skaitiklis jau atitinka esamą bloką. Prasidėdama nuo šio skaitiklio, ji pakeisdavo raktą ir sunaudodavo visą 1024 baitų raktų srautą, kuris neturėjo būti naudojamas, ir taip pastumdavo einamąją poziciją. Nuo to momento dešifratorius nukrypdavo per pilną bloką. Verifikatorius, patikrintas prieš tai, vis tiek praeidavo, todėl slaptažodis atrodė teisingas, o visi duomenų langeliai pavirsdavo šiukšlėmis.
Ištaisyta logika saugoma TXLSDecrypterRC4. Tiek Skip, tiek Decrypt dalijasi vienu ciklu: keisti raktą tik tada, kai einamoji pozicija pereina į naują bloką (kur bloko indeksas yra pozicija, padalinta iš 1024), tada sunaudoti reikšmes iki esamo bloko pabaigos ir ne daugiau. MakeKey iškviečiama su bloko indeksu, o pozicija pasistumia tiksliai per apdorotų baitų skaičių, kad Skip ir Decrypt liktų suderinti su gamintoju. Pamoka čia maža: vienas prarastas baitas srauto šifre yra ne maža klaida, o visiškas visko praradimas žemiau jo.
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;
Suderinamumas su užšaldyta specifikacija yra atitikimas iki baito
Abi klaidos susiveda į tą patį principą. Kai jūsų išvesties vartotojas yra fiksuota išorinė programa, kurios negalite pakeisti, šifravimo režimas ir raktų keitimo dažnis nėra jūsų pasirinkimo ar optimizavimo klausimas. Tai yra sutarties dalis. Excel dešifruos su ECB ir keis raktus ties 1024 baitų ribomis, nepriklausomai nuo to, ar šie pasirinkimai jums patinka, ar ne, ir jūsų vienintelis darbas yra sugeneruoti baitus, kurie dešifruotųsi pagal šią procedūrą. Režimas, kuris atrodo modernesnis, ar skaitiklis, kuris prasideda ten, kur patogu – bet kas tampa defektu tą pačią akimirką, kai nukrypsta nuo to, ko tikisi skaitytuvas. Suderinamumas su užšaldyta specifikacija nėra apytikris – jis yra tikslus iki baito arba neveikiantis išvis.
Dėl to verifikatorius vienas pats yra prastas testas. Jis tik parodo, kad rakto gavimas veikia, kas yra būtina, bet nepakankama sąlyga. Testas, kuris tik atidaro užšifruotą failą ir patvirtina slaptažodį, praneš apie sėkmę, nors failo turinys liks neįskaitomas. Tikrasis testas dešifruoja paketą ir palygina gautus baitus su originalu. Verifikatorius įrodo slaptažodį – tik failo kūnas įrodo šifravimą.
Palaikomas apsaugotų darbaknygių skaitymo ir rašymo būdas
Viešasis paviršius yra nedidelis. Norėdami įrašyti slaptažodžiu apsaugotą XLSX darbaknygę, atidarykite TXLSXWorkbook ir iškvieskite SaveAsEncrypted su failo pavadinimu bei slaptažodžiu – ji serializuoja darbaknygę ir paleidžia Standard Encryption procesą, kurį pataisė pirmasis sprendimas, grąžindama 1 sėkmės atveju. Norėdami nuskaityti, iškvieskite CanReadEncrypted patikrinimui, ar failas yra užšifruotas Compound File konteineris, ir tada nukreipkite kelią: OpenEncrypted apdoroja užšifruotą srautą ir grįžta prie Open paprastiems failams, o Open su slaptažodžiu yra prieinamas tiesiogiai. Režimų valdymas ir raktų keitimo ciklas, aprašyti aukščiau, veikia po šiais iškvietimais – jūs pateikiate slaptažodį bei failo pavadinimą, o variklis atlieka specifikacijos atitikimą už jus.
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.