Articol tehnic

De ce respinge Excel registrul dvs. de lucru criptat: ECB și RC4

Scrieți un registru de lucru, îl criptați cu o parolă, transmiteți fișierul unui coleg, iar acesta îl deschide în Excel. Excel solicită parola. Colegul o introduce, iar Excel o acceptă. Până în acest punct, criptarea pare corectă. Apoi, Excel afișează o casetă de dialog care anunță că fișierul este corupt și nu poate fi deschis, sau deschide o foaie de calcul cu celule lipsite de sens. Parola a fost corectă, însă fișierul este totuși nefuncțional. Acesta este cel mai derutant mod de eșec din criptarea Office, deoarece partea care confirmă corectitudinea parolei și partea care conține datele propriu-zise sunt protejate prin două operațiuni diferite, iar validarea uneia nu garantează succesul celeilalte

Ambele erori descrise aici au avut exact această structură. În fiecare caz, verificatorul a fost validat, însă corpul documentului nu, ceea ce vă trimite în căutarea unei erori de parolă sau de derivare a cheii care nu există în realitate. Problema reală se afla în aval, în modul în care au fost transformați octeții pachetului. Cele două defecțiuni sunt independente, una pe calea AES și cealaltă pe calea RC4, dar împart aceeași problemă de diagnosticare, motiv pentru care merită analizat de ce un rezultat pe jumătate corect este cel mai dificil de interpretat

De ce o parolă validată nu dovedește nimic despre corpul documentului

Formatul pe care îl utilizează fișierele moderne XLSX criptate este ECMA-376 Standard Encryption și stochează două elemente criptate unul lângă celălalt. Primul este EncryptionVerifier: un bloc de mici dimensiuni care conține o valoare aleatorie și hash-ul acelei valori, criptat cu cheia derivată din parolă. Al doilea este EncryptedPackage: întregul container zip al registrului de lucru, criptat cu aceeași cheie. Verificatorul există pentru ca un cititor să poată confirma o parolă înainte de a procesa megaocteți de conținut. Decriptați verificatorul, rulați hash pe valoarea aleatorie, comparați-o cu hash-ul stocat și, dacă se potrivesc, parola este corectă

Capcana este că verificatorul și pachetul sunt criptate prin apeluri separate pe buffere separate. O cheie derivată corect va decripta corespunzător verificatorul, indiferent de ceea ce se întâmplă cu pachetul ulterior. Așadar, dacă derivarea cheii este corectă, dar transformarea pachetului este defectuoasă, Excel confirmă parola prin verificator și apoi eșuează la încărcarea corpului. Simptomul se manifestă ca „parolă corectă, fișier corupt”, direcționând investigația către procesul de parolă, care era singura parte ce funcționa corect. Aceeași separare este valabilă și în cazul versiunii mai vechi RC4: hash-ul verificatorului este verificat mai întâi, iar un corp care se decalează păstrează totuși acea verificare intactă

Prima eroare: AES în ECB, nu în CBC

[MS-OFFCRYPTO] §2.3.4.15 specifică faptul că Standard Encryption criptează pachetul cu AES în modul Electronic Codebook. Fiecare bloc de 16 octeți al pachetului completat este criptat independent cu aceeași cheie. Nu există înlănțuire între blocuri și nu există un vector de inițializare. Aceasta este o alegere neobișnuită pentru standardele moderne, unde modul ECB este de regulă evitat, însă interoperabilitatea nu permite modificarea specificațiilor. Excel decriptează pachetul ca ECB, prin urmare un generator trebuie să îl cripteze ca ECB, altfel cele două nu se vor potrivi

Eroarea a constat în faptul că pachetul a fost criptat cu AES în modul CBC, folosind un vector de inițializare format numai din zerouri. Iată de ce această abordare funcționează parțial și de ce o funcționare parțială este cel mai prost scenariu. În CBC, primul bloc de text clar este supus unei operații XOR cu IV-ul înainte de criptare. Când IV-ul conține doar zerouri, acea operațiune XOR nu modifică nimic, astfel încât primul bloc de CBC cu IV zero produce exact același text cifrat ca și ECB. Începând cu al doilea bloc, CBC transmite blocul anterior de text cifrat în următorul, astfel încât fiecare bloc după primul deviază de la ECB

Dacă aplicăm acest lucru pe structură, formatul pachetului plasează un prefix de lungime little-endian de 8 octeți chiar la început, astfel încât secțiunile pe care Excel le verifică cel mai devreme se află în primul sau în al doilea bloc. O potrivire a primului bloc înseamnă că validările inițiale trec, în timp ce fiecare bloc ulterior este decriptat ca zgomot. Soluția este evidentă odată ce modul este identificat: criptați fiecare bloc de 16 octeți cu ECB și opriți înlănțuirea. În motorul de calcul, funcția XlsEncryptStdPackage parcurge bufferul completat în pași de 16 octeți și apelează AESEncryptECB128Block pentru fiecare, fiind aceeași primitivă utilizată deja pentru blocurile verificatorului. Sursa conține un comentariu la nivelul buclei care precizează clar regula: CBC cu un IV zero se potrivește cu ECB doar pentru primul bloc, astfel încât restul pachetului s-ar decripta ca date nefolositoare, iar Excel l-ar respinge

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;

A doua eroare: cheia RC4 se decalează din ritm

Calea veche de fișiere .xls utilizează schema RC4 CryptoAPI, iar regula sa este diferită. [MS-OFFCRYPTO] §2.3.6 specifică faptul că cifrul este re-cheiat (re-keyed) la fiecare graniță de bloc de 1024 de octeți. Fluxul este împărțit în blocuri de 1024 de octeți, o cheie RC4 nouă este derivată pentru blocul numărul 0, 1, 2 și așa mai departe, iar în interiorul fiecărui bloc fluxul de chei (keystream) este consumat continuu, octet cu octet. Două reguli constante trebuie să funcționeze împreună: regenerarea cheii la fiecare graniță și consumul fluxului de chei fără întreruperi în interiorul unui bloc. RC4 este un cifru de flux, deci fluxul său de chei este o singură secvență ordonată; al n-lea octet extras este determinat de câți octeți au fost extrași înaintea lui. Decriptarea reprezintă aceeași operațiune XOR pe aceeași secvență, ceea ce înseamnă că generatorul și consumatorul trebuie să extragă exact aceiași octeți de la aceleași poziții

Aceasta este întreaga dificultate. Un cifru de flux nu se resincronizează. Dacă pierdeți un singur octet din fluxul de chei, fiecare octet de după el este supus XOR cu un octet greșit din fluxul de chei, iar eroarea nu se corectează niciodată; ea continuă până la sfârșitul blocului și, odată ce poziția curentă devine greșită, se propagă la fiecare bloc următor. Eroarea de față a făcut exact acest lucru. Contorul de blocuri a început de la o valoare santinelă de minus unu, iar rutina de skip a presupus că contorul se potrivea deja cu blocul curent. Pornind de la acea santinelă, a regenerat cheia și a rulat un bloc complet de 1024 de octeți din fluxul de chei care nu ar fi trebuit niciodată consumat, iar în acest proces a transformat numărul rămas într-o valoare negativă. Din acel moment, decriptorul a funcționat decalat cu un bloc întreg. Verificatorul, analizat înaintea tuturor acestor pași, a trecut testul, astfel încât parola părea corectă în timp ce fiecare celulă de date se decripta ca informație eronată

Logica corectată se află în clasa TXLSDecrypterRC4. Atât Skip, cât și Decrypt partajează o singură buclă: se regenerează cheia doar atunci când poziția de rulare trece într-un bloc nou, unde indexul blocului reprezintă poziția împărțită la REKEY_BLOCK_SIZE (1024), apoi se consumă până la restul blocului curent și nimic mai mult. Funcția MakeKey este apelată cu indexul blocului, niciodată cu un index expirat sau santinelă, iar poziția avansează cu numărul exact de octeți procesați, astfel încât Skip și Decrypt să rămână aliniate în fază cu generatorul. Învățămintele se regăsesc în cea mai mică unitate: un singur octet pierdut nu reprezintă o eroare minoră într-un cifru de flux, ci o pierdere totală a datelor din aval

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;

Interoperabilitatea cu o specificație fixă înseamnă potrivire la nivel de octet

Ambele erori se reduc la același principiu fundamental, care merită menționat separat deoarece schimbă modul în care evaluați deciziile de proiectare. Când consumatorul datelor de ieșire este un program extern fix pe care nu îl puteți modifica, modul de cifrare și cadența de re-cheiere nu sunt detalii de implementare pe care le puteți optimiza sau simplifica. Acestea fac parte din contractul de comunicare directă. Excel va decripta folosind modul ECB și va regenera cheia la limite de 1024 de octeți, indiferent dacă aceste opțiuni vă convin sau nu, iar singura dvs. sarcină este să generați octeți care să fie decriptați conform documentului original prin acea procedură exactă. Un mod mai modern, un IV care pare inofensiv, un contor care începe de unde pare natural — oricare dintre acestea devine un defect în momentul în care deviază de la ceea ce se așteaptă cititorul. Interoperabilitatea în raport cu o specificație fixă nu este aproximativă, ci trebuie să fie exactă la nivel de octet

Acesta este și motivul pentru care verificatorul nu reprezintă un test rapid suficient de unul singur. El confirmă doar funcționarea derivării cheii, lucru necesar, dar departe de a fi suficient. Un test care se limitează la deschiderea unui fișier criptat și confirmarea parolei va raporta succesul chiar dacă corpul documentului este ilizibil. Un test complet decriptează pachetul și compară octeții recuperați cu datele de intrare originale, sau trece registrul de lucru prin procesul complet de criptare-decriptare și citește din nou celulele. Verificatorul dovedește parola; doar corpul documentului dovedește corectitudinea criptării

Calea recomandată pentru citirea și scrierea registrelor de lucru protejate

Interfața grafică este restrânsă. Pentru a scrie un registru de lucru modern protejat prin parolă, completați sau deschideți un obiect TXLSXWorkbook și apelați SaveAsEncrypted cu un nume de fișier și o parolă; această metodă serializează registrul de lucru și execută linia de procesare Standard Encryption corectată de prima remediere, returnând 1 în caz de succes. Pentru citire, apelați CanReadEncrypted pentru a verifica dacă fișierul este un container criptat Compound File, apoi direcționați: OpenEncrypted gestionează calea criptată și recurge la Open pentru fișierele simple, iar Open cu parolă este disponibilă direct. Gestionarea modului și bucla de re-cheiere descrise mai sus sunt poziționate sub aceste apeluri; dvs. furnizați parola și numele fișierului, iar motorul de calcul respectă specificațiile în locul dvs

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;

Structura rezultatului protejat, fluxul EncryptionInfo, blocurile verificatorului și formatul pachetului sunt acoperite în prezentarea detaliată a rezultatelor XLSX protejate prin AES. Pentru problema separată a blocării la nivel de foaie și modul în care protecția interacționează cu configurarea paginii și imprimarea, consultați articolul despre protecție, configurarea paginii și imprimare. Ambele se bazează pe procesul de criptare descris aici, care este livrat ca parte a componentei HotXLS pentru Delphi și C++Builder, alături de API-urile de citire, scriere și randare prezentate în alte secțiuni ale acestui blog