Teknisk artikkel

Les Agile-krypterte Excel-filer i Delphi med HotXLS

HotXLS leser Agile-krypterte Excel-filer, passordbeskyttelsen som Excel 2010 og alle senere versjoner bruker som standard, gjennom ett enkelt kall: TXLSXWorkbook.OpenEncrypted. Komponenten tolker XML-krypteringsdeskriptoren, avleder nøkler fra passordet med en SHA-512-spinCount-hashkjede, verifiserer passordet mot den krypterte verifikatoren, og dekrypterer deretter pakken i 4096-byte AES-CBC-segmenter. Ingen Excel-installasjon, ingen COM, ingen ekstern krypto-DLL er involvert

Denne artikkelen dekker spesifikt lesesiden av Agile-kryptering. To beslektede problemstillinger har sine egne artikler: interoperabilitet med de eldre RC4- og XOR-skjemaene inne i gamle BIFF-.xls-filer er dekket i artikkelen om ECB- og RC4-interoperabilitet, og det å produsere passordbeskyttede arbeidsbøker med ECMA-376 Standard Encryption er dekket i artikkelen om AES-beskyttet XLSX-utdata. Her eksisterer filen allerede, noen andre krypterte den, og din jobb er å åpne den

Scenarioet som tvinger fram problemstillingen er kjent for alle som kjører en dokumentpipeline. En serverside-importtjeneste tar imot opplastede arbeidsbøker; det finnes ingen Excel på maskinen, og det vil aldri gjøre det; og en morgen laster en kunde opp en helt ordinær .xlsx-fil som ZIP-leseren avviser fordi den ikke er en ZIP-fil i det hele tatt. Kunden lagret den med et passord. Fra det øyeblikket må lasteren din enten forstå [MS-OFFCRYPTO], eller så sender den filen tilbake til en bruker som, fra sitt ståsted, ikke gjorde noe uvanlig

Hva er Agile-kryptering i en Excel-fil?

Agile-kryptering er passordbeskyttelsesskjemaet definert i [MS-OFFCRYPTO] §2.3.4.10 til §2.3.4.15, og det er det Excel 2010 og senere skriver når en arbeidsbok lagres med passord. Den krypterte filen er ikke lenger en ZIP-pakke. Det er en OLE Compound File Binary-beholder (CFB) som inneholder to strømmer: EncryptionInfo, som beskriver hvordan krypteringen ble utført, og EncryptedPackage, som er selve .xlsx-ZIP-en kryptert som en ugjennomsiktig blob. CFB-signaturen (D0 CF 11 E0 A1 B1 1A E1) er den samme magiske sekvensen som eldre BIFF-.xls-filer bærer, som er grunnen til at en omdøpt eller kryptert fil ikke kan klassifiseres ut fra filendelsen alene

Anatomi av den Agile-krypterte arbeidsboken som HotXLS åpner i Delphi: en CFB-container som holder en EncryptionInfo-deskriptorstrøm og en EncryptedPackage-strøm av AES-CBC-segmenter
HotXLS leser en Agile-kryptert arbeidsbok som en CFB-beholder med en selvbeskrivende EncryptionInfo-strøm og .xlsx-ZIP-en kryptert som en ugjennomsiktig EncryptedPackage-strøm

Det som skiller Agile fra sine forgjengere er at EncryptionInfo er selvbeskrivende. Etter en 8-byte versjonsprefiks, der både hoved- og underversjon er 4, er strømmen en UTF-8 XML-deskriptor. Et keyData-element erklærer chifferet (AES), kjedemodusen (ChainingModeCBC), hashen (SHA512), nøkkellengden i biter, blokkstørrelsen, og et Base64-salt. Et keyEncryptor-element for passordet bærer sitt eget salt, spinCount, og tre Base64-nyttelaster: encryptedVerifierHashInput, encryptedVerifierHashValue, og encryptedKeyValue. Excel skriver AES-256 med et spinCount på 100 000, men deskriptoren har lov til å erklære AES-128 eller AES-192, og HotXLS følger det keyBits sier i stedet for å anta 256

Ett inngangspunkt for klartekst-, Standard- og Agile-arbeidsbøker

TXLSXWorkbook.OpenEncrypted håndterer alle de tre tilstandene en kaller kan møte, ren ZIP, Standard-kryptert og Agile-kryptert, slik at opplastingshåndterere ikke trenger å klassifisere filer før de laster dem. Metoden snuser først på filen: hvis det ikke finnes en CFB-signatur, overlater den til den vanlige Open-banen, og passordet ignoreres ganske enkelt. Hvis filen er en CFB-beholder, prøver den ECMA-376 Standard Encryption først, og når EncryptionInfo-versjonssignaturen er Agile 4.4, sender den videre til Agile-pipelinen. Returverdien er 1 ved suksess, samme kontrakt som Open

var
  Wb: TXLSXWorkbook;
begin
  Wb := TXLSXWorkbook.Create;
  try
    // Fungerer for vanlig .xlsx, Standard-kryptert og
    // Agile-kryptert filer likt
    if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
      Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
  finally
    Wb.Free;
  end;
end;

Reserveløsningen for ukryptert inndata betyr mer enn det ser ut som. En batch-importør som alltid kaller OpenEncrypted trenger ingen forgrening ved kallstedet: filer som aldri var beskyttet lastes akkurat som før, og filer som ankommer kryptert dekrypteres på stedet og mates deretter til den vanlige ZIP-lasteren som en strøm i minnet. Det er én kodesti å teste, ikke tre

Hvordan blir et passord til en AES-nøkkel?

Agile-kryptering bruker aldri passordet direkte. HotXLS beregner først en iterert hash: den innledende digesten er SHA-512 over passordsaltet sammenkjedet med UTF-16LE-bytene til passordet, og deretter hashes digesten på nytt spinCount ganger, der hver runde setter den 32-biters little-endian iterasjonstelleren foran forrige digest. Med Excels standard spinCount på 100 000 er det hundre tusen serielle SHA-512-kall per passordforsøk, og det er hele poenget. SpinCount fungerer som en brute-force-brems: det koster en legitim kaller noen få millisekunder én gang, og koster en ordbokangriper de samme få millisekundene for hvert eneste gjetteforsøk

Hvordan HotXLS utleder Agile krypteringsnøkler i Delphi: passordet spunnes gjennom 100 000 SHA-512-runder, deretter gir tre faste 8-byte blokknøkler verifikatoren og pakkenøklene
Passordet dekrypterer aldri noe direkte: en SHA-512 spin-count-kjede mater tre blokknøkkel-hasher som produserer verifikatornøklene og pakker opp den tilfeldige pakkenøkkelen
// [MS-OFFCRYPTO] iterert passord-hash:
//   H(0) = SHA-512(salt + UTF-16LE(passord))
//   H(n) = SHA-512(LE32(n - 1) + H(n - 1)), gjentatt spinCount ganger
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);            // iterasjonsteller, little-endian
    Move(Result[0], buf[4], 64);   // forrige digest
    Result := XlsSHA512(buf);
  end;
end;

Den spunne hashen er fortsatt ikke en nøkkel. Tre distinkte nøkler avledes fra den ved å hashe den én gang til med en fast 8-byte blokknøkkel lagt til, én konstant per formål: FE A7 D2 76 3B 4B 9E 79 for å dekryptere verifikator-inndataen, D7 AA 0F 6D 30 61 34 4E for verifikator-hashen, og 14 6E 0B E7 AB AC D0 D6 for å pakke opp selve pakkenøkkelen. Hvert SHA-512-resultat trunkeres til den erklærte nøkkellengden, og, i henhold til [MS-OFFCRYPTO], fylles ut med 0x36-byte i det teoretiske tilfellet der hashen er kortere enn nøkkelen. Den samme 0x36-utfyllingsregelen gjelder når passordsaltet utvides til blokkstørrelse for bruk som CBC-initialiseringsvektor

Passordverifisering og saltSize-trunkeringsfellen

HotXLS verifiserer passordet før det rører pakken, ved å bruke verifikatorparet fra deskriptoren. Den dekrypterer encryptedVerifierHashInput med den første avledede nøkkelen, hasher resultatet med SHA-512, dekrypterer encryptedVerifierHashValue med den andre avledede nøkkelen, og sammenligner de to digestene byte for byte. Et avvik betyr at passordet er feil, rapportert som et distinkt utfall i stedet for en forvrengt arbeidsbok, og avgjørende er det at pakkeinnholdet aldri dekrypteres med en feil nøkkel, så det finnes ikke noe scenario der et feil passord produserer plausibelt utseende korrupte data

Det er en spesifikasjonsdetalj her som er lett å gjøre feil. [MS-OFFCRYPTO] §2.3.4.13 definerer verifikatoren som saltSize byte med tilfeldige data, der saltSize er lengden på nøkkelkrypteringens salt, ikke chifferets blokkstørrelse. Fordi AES-CBC-chiffertekst er blokkjustert, kommer den dekrypterte verifikator-inndataen tilbake fylt ut til et multiplum av 16 byte, og den må trunkeres tilbake til saltSize før hashing. Excel skriver alltid saltSize lik blockSize, begge 16, så en implementasjon som hopper over trunkeringen består hver test mot ekte Excel-utdata, og feiler deretter på den første filen fra en produsent som valgte en annen saltlengde. HotXLS trunkerer til saltlengden fordi det er det spesifikasjonen faktisk sier, og at de to verdiene stemmer overens i praksis er en tilfeldighet, ikke en kontrakt

Hvordan dekrypteres EncryptedPackage?

EncryptedPackage-strømmen begynner med en 8-byte little-endian klartekststørrelse, etterfulgt av chifferteksten i 4096-byte segmenter, og HotXLS dekrypterer den segment for segment med en fersk IV per segment. Selve pakkenøkkelen er ikke avledet fra passordet: det er en tilfeldig mellomnøkkel som skriveren krypterte inn i encryptedKeyValue, og HotXLS pakker den opp med den tredje avledede nøkkelen, trunkert til nøkkellengden erklært av keyData. IV-en til hvert segment er SHA-512 over keyData-saltet sammenkjedet med den 32-biters little-endian segmentindeksen, trunkert til blokkstørrelsen. Denne konstruksjonen betyr at ethvert 4096-byte segment kan dekrypteres uavhengig, noe som også er det som gjør formatet vennlig for tilfeldig tilgang i prinsippet, selv om HotXLS dekrypterer hele pakken til minnet og overleverer de resulterende ZIP-bytene til sin vanlige XLSX-laster

HotXLS dekrypterer EncryptedPackage-strømmen i Delphi segment for segment, og utleder hver 4096-byte AES-CBC-segment-IV fra keyData-salten og segmentindeksen
Hvert 4096-byte AES-CBC-segment dekrypteres uavhengig under sin egen salt-og-indeks-IV, og resultatet avkortes til den deklarerte klartekststørrelsen

Den erklærte klartekststørrelsen gjør den siste delen av jobben. AES-CBC-utdata er blokkjustert, så det siste segmentet bærer opptil 15 byte med utfylling som ikke er en del av dokumentet; den dekrypterte bufferen trunkeres til størrelsesprefikset, og resultatet er nøyaktig den .xlsx-ZIP-en Excel krypterte. HotXLS validerer prefikset mot den faktiske strømlengden før dekryptering, slik at en avkuttet opplasting eller et manipulert størrelsesfelt feiler rent i stedet for å overskride grensen

Feilrapportering og ærlige grenser

Feilmodusene holdes bevisst atskilt. Et feil passord kaster et unntak med en eksplisitt feil-passord-melding, drevet av verifikatoravviket, slik at et brukergrensesnitt kan be brukeren prøve på nytt. En CFB-beholder der deskriptoren erklærer algoritmer utenfor det støttede settet, noe annet enn AES med CBC-kjeding og SHA-512-hashing i en Agile-deskriptor, eller en beholder som verken er Standard eller Agile, kaster et annet unntak som identifiserer skjemaet som ikke støttet. De to må aldri blandes sammen: å prøve et passord på nytt mot et ikke-støttet skjema kaster bort brukerens tid, og å rapportere et feil passord som en formatfeil sender støtteteamet ditt på feil spor

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
      // Kastes både for feil passord og et ikke-støttet
      // skjema; E.Message angir hvilket, så logg det ordrett og
      // tilby bare et passordforsøk på nytt for feil-passord-tilfellet
      RejectUpload(FileName, E.Message);
  end;
end;

Grensene er verdt å si rett ut. HotXLS leser Agile-deskriptorer som erklærer AES i CBC-modus med SHA-512, noe som dekker det Excel 2010 til og med Excel 365 faktisk skriver, i alle tre nøkkelstørrelsene. Deskriptorer som erklærer andre chiffer eller hash-algoritmer avvises i stedet for å bli gjettet på, og sertifikatbaserte nøkkelkrypteringer konsulteres ikke, kun passord-nøkkelkrypteringen brukes. På skrivesiden produserer HotXLS for øyeblikket Standard Encryption i stedet for Agile, et skille som betyr noe hvis nedstrøms verktøy inspiserer skjemaet; detaljene finnes i artikkelen om å skrive AES-beskyttet XLSX-utdata

Passordbeskyttede opplastinger slutter å være et spesialtilfelle når lasteren behandler kryptering som en del av filformatet i stedet for et unntak fra det. Inngangspunktet OpenEncrypted, SHA-512-spinCount-avledningen og den segmenterte AES-CBC-pipelinen som er beskrevet her, leveres som en del av HotXLS Delphi Excel Component, sammen med resten av dens native XLS- og XLSX-lese- og skrivemotor for Delphi og C++Builder