Articol tehnic

Citirea fișierelor Excel criptate prin Agile în Delphi cu HotXLS

HotXLS citește fișierele Excel criptate prin Agile — protecția prin parolă pe care Excel 2010 și toate versiunile ulterioare o aplică în mod implicit — printr-un singur apel: TXLSXWorkbook.OpenEncrypted. Componenta analizează descriptorul de criptare XML, derivă cheile din parolă printr-un lanț de hash-uri SHA-512 cu număr de rotații, verifică parola în raport cu verificatorul criptat și apoi decriptează pachetul în segmente AES-CBC de 4096 de octeți. Nu este necesară nicio instalare Excel, COM sau DLL criptografic extern

Acest articol acoperă în mod specific partea de citire a criptării Agile. Două probleme conexe au propriile articole: interoperabilitatea cu schemele vechi RC4 și XOR din fișierele vechi BIFF .xls este acoperită în articolul despre interoperabilitatea ECB și RC4, iar generarea de registre de lucru protejate prin parolă cu criptarea ECMA-376 Standard Encryption este tratată în articolul despre rezultatele XLSX protejate prin AES. În acest caz, fișierul există deja, altcineva l-a criptat, iar sarcina dvs. este să îl deschideți

Scenariul care impune această problemă este familiar oricărei persoane care gestionează o linie de procesare a documentelor. Un serviciu de import pe server acceptă încărcări de registre de lucru; nu există Excel pe mașină și nu va exista niciodată; iar într-o dimineață un client încarcă un fișier .xlsx absolut obișnuit pe care cititorul ZIP îl respinge deoarece nu este deloc un fișier ZIP. Clientul l-a salvat cu o parolă. Din acel moment, încărcătorul dvs. fie înțelege [MS-OFFCRYPTO], fie returnează fișierul unui utilizator care, din punctul său de vedere, nu a făcut nimic neobișnuit

Ce este criptarea Agile într-un fișier Excel?

Criptarea Agile este schema de protecție prin parolă definită în [MS-OFFCRYPTO] §2.3.4.10 până la §2.3.4.15 și este ceea ce scrie Excel 2010 și versiunile ulterioare ori de câte ori un registru de lucru este salvat cu parolă. Fișierul criptat nu mai este un pachet ZIP. Este un container OLE Compound File Binary (CFB) care conține două fluxuri: EncryptionInfo, care descrie modul în care a fost realizată criptarea, și EncryptedPackage, care este fișierul real ZIP .xlsx criptat ca un blob opac. Semnătura CFB (D0 CF 11 E0 A1 B1 1A E1) este aceeași semnătură magică pe care o conțin fișierele vechi BIFF .xls, motiv pentru care un fișier redenumit sau criptat nu poate fi clasificat doar după extensia sa

Ceea ce diferențiază Agile de predecesorii săi este că fluxul EncryptionInfo este auto-descriptiv. După un prefix de versiune de 8 octeți, cu versiunea principală și secundară ambele fiind 4, fluxul este un descriptor XML UTF-8. Un element keyData declară cifrul (AES), modul de înlănțuire (ChainingModeCBC), hash-ul (SHA512), lungimea cheii în biți, dimensiunea blocului și o sare (salt) Base64. Un element keyEncryptor de parolă conține propria sare, valoarea spinCount și trei sarcini utile Base64: encryptedVerifierHashInput, encryptedVerifierHashValue și encryptedKeyValue. Excel scrie AES-256 cu un număr de rotații (spin count) de 100.000, însă descriptorul poate declara AES-128 sau AES-192, iar HotXLS respectă valoarea specificată de keyBits, fără a presupune automat 256

Un singur punct de intrare pentru registrele de lucru text simplu, Standard și Agile

Metoda TXLSXWorkbook.OpenEncrypted gestionează toate cele trei stări pe care le poate întâlni un apelant — ZIP simplu, criptat Standard și criptat Agile — astfel încât instrumentele de încărcare nu trebuie să clasifice fișierele înainte de a le încărca. Metoda verifică mai întâi fișierul: dacă nu există o semnătură CFB, recurge la calea normală Open, iar parola este pur și simplu ignorată. Dacă fișierul este un container CFB, încearcă mai întâi criptarea ECMA-376 Standard Encryption, iar când semnătura versiunii EncryptionInfo este Agile 4.4, trimite procesul către linia de procesare Agile. Valoarea returnată este 1 în caz de succes, același contract ca și la 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;

Soluția de rezervă pentru intrările necriptate contează mai mult decât pare. Un importator pe loturi care apelează întotdeauna OpenEncrypted nu are nevoie de ramificare la nivel de apel: fișierele care nu au fost niciodată protejate se încarcă exact ca înainte, iar fișierele care sosesc criptate sunt decriptate pe loc și apoi transmise încărcătorului ZIP obișnuit ca un flux în memorie. Există o singură cale de cod de testat, nu trei

Cum devine o parolă o cheie AES?

Criptarea Agile nu utilizează niciodată parola în mod direct. HotXLS calculează mai întâi un hash iterat: rezumatul inițial este SHA-512 peste sarea parolei concatenată cu octeții UTF-16LE ai parolei, iar apoi rezumatul este re-hash-uit de spinCount ori, fiecare rundă adăugând contorul de iterații little-endian pe 32 de biți la rezumatul anterior. Cu numărul implicit de rotații din Excel de 100.000, avem o sută de mii de apelări SHA-512 în serie pentru fiecare încercare de parolă, acesta fiind și scopul principal. Numărul de rotații este o limitare a forței brute: costă un apelant legitim câteva milisecunde o singură dată și costă un atacator prin dicționar aceleași câteva milisecunde pentru fiecare încercare

// [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;

Hash-ul obținut încă nu este o cheie. Trei chei distincte sunt derivate din acesta prin rularea hash-ului încă o dată cu o cheie de bloc fixă de 8 octeți adăugată la final, câte o constantă pentru fiecare scop: FE A7 D2 76 3B 4B 9E 79 pentru decriptarea intrării verificatorului, D7 AA 0F 6D 30 61 34 4E pentru hash-ul verificatorului și 14 6E 0B E7 AB AC D0 D6 pentru desfacerea cheii efective a pachetului. Fiecare rezultat SHA-512 este trunchiat la lungimea cheii declarate și, conform [MS-OFFCRYPTO], este completat cu octeți 0x36 în cazul teoretic în care hash-ul este mai scurt decât cheia. Aceeași regulă de umplere cu 0x36 se aplică atunci când sarea parolei este extinsă la dimensiunea blocului pentru a fi utilizată ca vector de inițializare CBC

Verificarea parolei și capcana trunchierii saltSize

HotXLS verifică parola înainte de a accesa pachetul, utilizând perechea de verificatori din descriptor. Decriptează encryptedVerifierHashInput cu prima cheie derivată, rulează hash pe rezultat cu SHA-512, decriptează encryptedVerifierHashValue cu a doua cheie derivată și compară cele două rezumate octet cu octet. O nepotrivire înseamnă că parola este greșită, acest lucru fiind raportat ca un rezultat distinct în loc de un registru de lucru corupt, și în mod esențial înseamnă că pachetul principal nu este decriptat niciodată cu o cheie greșită, neexistând niciun scenariu în care o parolă greșită să producă date corupte care să pară plauzibile

Există aici un detaliu de specificație care poate fi ușor interpretat greșit. [MS-OFFCRYPTO] §2.3.4.13 definește verificatorul ca fiind format din saltSize octeți de date aleatorii, unde saltSize este lungimea sării criptorului cheii, nu dimensiunea blocului cifrului. Deoarece textul cifrat AES-CBC este aliniat la nivel de bloc, intrarea decriptată a verificatorului este returnată umplută până la un multiplu de 16 octeți și trebuie trunchiată înapoi la valoarea saltSize înainte de rularea hash. Excel scrie întotdeauna saltSize egal cu blockSize, ambele având valoarea 16, astfel încât o implementare care omite trunchierea trece toate testele pe fișiere reale Excel și apoi eșuează la primul fișier de la un generator care a ales o lungime diferită a sării. HotXLS trunchiază valoarea la lungimea sării deoarece așa prevede specificația în realitate, iar potrivirea celor două valori în practică este o coincidență, nu o regulă

Cum se decriptează EncryptedPackage?

Fluxul EncryptedPackage începe cu o lungime de text simplu little-endian pe 8 octeți, urmată de textul cifrat în segmente de 4096 de octeți, iar HotXLS îl decriptează segment cu segment, utilizând un IV nou pentru fiecare segment. Cheia pachetului în sine nu este derivată din parolă: este o cheie intermediară aleatorie pe care programul de scriere a criptat-o în encryptedKeyValue, iar HotXLS o desface cu a treia cheie derivată, trunchind-o la lungimea cheii declarate de keyData. IV-ul fiecărui segment este SHA-512 peste sarea keyData concatenată cu indexul segmentului little-endian pe 32 de biți, trunchiat la dimensiunea blocului. Această structură înseamnă că orice segment de 4096 de octeți poate fi decriptat independent, ceea ce face ca formatul să permită accesul aleatoriu în principiu, deși HotXLS decriptează întregul pachet în memorie și transmite octeții ZIP rezultați încărcătorului său obișnuit XLSX

Dimensiunea declarată a textului simplu realizează ultima parte a procesului. Rezultatul AES-CBC este aliniat la bloc, astfel încât ultimul segment conține până la 15 octeți de umplere (padding) care nu fac parte din document; buffer-ul decriptat este trunchiat la prefixul de dimensiune, iar rezultatul este exact fișierul ZIP .xlsx pe care l-a criptat Excel. HotXLS validează prefixul în raport cu lungimea reală a fluxului înainte de decriptare, astfel încât o încărcare trunchiată sau un câmp de dimensiune modificat neautorizat determină un eșec curat, evitând depășirea limitelor

Raportarea erorilor și limitele reale

Modurile de eșec sunt separate în mod deliberat. O parolă greșită generează o excepție cu un mesaj explicit de parolă greșită, pe baza nepotrivirii verificatorului, astfel încât interfața grafică poate solicita utilizatorului să încerce din nou. Un container CFB al cărui descriptor declară algoritmi din afara setului acceptat, adică orice altceva în afară de AES cu înlănțuire CBC și hash SHA-512 într-un descriptor Agile, sau un container care nu este nici Standard, nici Agile, generează o excepție diferită care identifică schema ca fiind neacceptată. Cele două nu trebuie confundate niciodată: încercarea repetată a unei parole pe o schemă neacceptată risipește timpul utilizatorului, iar raportarea unei parole greșite ca fiind o eroare de format direcționează greșit echipa de asistență

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;

Limitele merită menționate în mod clar. HotXLS citește descriptori Agile care declară AES în modul CBC cu SHA-512, ceea ce acoperă ceea ce scriu de fapt Excel 2010 până la Excel 365, în toate cele trei dimensiuni de chei. Descriptorii care declară alte cifruri sau algoritmi de hash sunt respinși în loc de a se face presupuneri, iar criptorii de chei bazați pe certificate nu sunt consultați, ci doar criptorul de cheie pe bază de parolă. Pe partea de scriere, HotXLS produce în prezent Standard Encryption în loc de Agile, o distincție care contează dacă instrumentele din aval analizează schema; detaliile se găsesc în articolul de spre generarea rezultatelor XLSX protejate prin AES

Încărcările protejate prin parolă nu mai reprezintă un caz special odată ce încărcătorul tratează criptarea ca parte a formatului de fișier, nu ca pe o excepție de la acesta. Punctul de intrare OpenEncrypted, derivarea SHA-512 cu număr de rotații și linia de procesare segmentată AES-CBC descrise aici sunt livrate ca parte a HotXLS Delphi Excel Component, alături de restul motorului său nativ de citire și scriere XLS și XLSX pentru Delphi și C++Builder