Teknisk artikel

Læs Agile-krypterede Excel-filer i Delphi med HotXLS

HotXLS læser Agile-krypterede Excel-filer — den adgangskodebeskyttelse, som Excel 2010 og alle senere versioner anvender som standard — via et enkelt kald: TXLSXWorkbook.OpenEncrypted. Komponenten fortolker XML-krypteringsbeskrivelsen, udleder nøgler fra adgangskoden med en SHA-512 spin-count hash-kæde, verificerer adgangskoden mod den krypterede verifikator og dekrypterer derefter pakken i 4096-byte AES-CBC-segmenter. Hverken Excel-installation, COM eller eksterne kryptografiske DLL-filer er involveret

Denne artikel dækker specifikt læsesiden af Agile-kryptering. To beslægtede emner har deres egne artikler: Interoperabilitet med de ældre RC4- og XOR-skemaer i gamle BIFF .xls-filer dækkes i artiklen om ECB- og RC4-interoperabilitet, og oprettelse af adgangskodebeskyttede arbejdsbøger med ECMA-376 Standard Encryption dækkes i artiklen om AES-beskyttet XLSX-output. Her eksisterer filen allerede, en anden har krypteret den, og din opgave er at åbne den

Det scenarie, der fremtvinger dette behov, er velkendt for alle, der driver en dokumentpipeline. En importtjeneste på serversiden modtager uploadede arbejdsbøger; der er ingen Excel på maskinen, og det kommer der heller ikke; og en morgen uploader en kunde en helt almindelig .xlsx-fil, som ZIP-læseren afviser, fordi det slet ikke er en ZIP-fil. Kunden gemte den med en adgangskode. Fra det øjeblik skal din loader enten forstå [MS-OFFCRYPTO], eller også må den sende filen retur to en bruger, som fra sit eget synspunkt ikke gjorde noget usædvanligt

Hvad er Agile-kryptering i en Excel-fil?

Agile-kryptering er det adgangskodebeskyttelsesskema, der er defineret i [MS-OFFCRYPTO] §2.3.4.10 til §2.3.4.15, og det er det, Excel 2010 og senere skriver, når en arbejdsbog gemmes med en adgangskode. Den krypterede fil er ikke længere en ZIP-pakke. Det er en OLE Compound File Binary (CFB)-beholder, der indeholder to strømme: EncryptionInfo, som beskriver, hvordan krypteringen blev udført, og EncryptedPackage, som er den rigtige .xlsx ZIP-fil krypteret som en uigennemsigtig (opaque) blob. CFB-signaturen (D0 CF 11 E0 A1 B1 1A E1) er den samme magiske kode, som ældre BIFF .xls-filer bærer, hvilket er grunden til, at en omdøbt eller krypteret fil ikke kan klassificeres alene ud fra filtypen

Det, der adskiller Agile fra dets forgængere, er, at EncryptionInfo er selvbeskrivende. Efter et 8-byte versionspræfiks, hvor både major og minor version er 4, er strømmen en UTF-8 XML-beskrivelse. Et keyData-element angiver krypteringen (AES), kædetilstanden (ChainingModeCBC), hashen (SHA512), nøglelængden i bits, blokstørrelsen og et Base64-salt. Et adgangskode-keyEncryptor-element indeholder sit eget salt, spinCount og tre Base64-dataenheder: encryptedVerifierHashInput, encryptedVerifierHashValue og encryptedKeyValue. Excel skriver AES-256 med et spin-antal på 100.000, men beskrivelsen må gerne angive AES-128 eller AES-192, og HotXLS respekterer, hvad keyBits angiver, frem for at antage 256

Ét adgangspunkt for klartekst, Standard- og Agile-arbejdsbøger

TXLSXWorkbook.OpenEncrypted håndterer alle tre tilstande, en kalder kan støde på: Klartekst ZIP, Standard-krypteret og Agile-krypteret, så upload-håndteringsrutiner ikke behøver at klassificere filer, før de indlæses. Metoden snuser først til filen: Hvis der ikke er nogen CFB-signatur, delegerer den til den normale Open-sti, og adgangskoden ignoreres simpelthen. Hvis filen er a CFB-beholder, prøver den ECMA-376 Standard Encryption først, og når EncryptionInfo versionssignaturen er Agile 4.4, sendes den videre til Agile-pipelinen. Returværdien er 1 ved succes, hvilket er samme kontrakt som for 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;

Fallback-løsningen for ukrypteret input er vigtigere, end det umiddelbart ser ud til. En batch-importer, der altid kalder OpenEncrypted, kræver ingen forgrening på kalder-stedet: Filer, der aldrig har været beskyttet, indlæses nøjagtigt som før, og filer, der ankommer krypteret, dekrypteres på stedet og fødes derefter til den almindelige ZIP-læser som en strøm i hukommelsen. Der er kun én kodesti at teste, ikke tre

Hvordan bliver en adgangskode til en AES-nøgle?

Agile-kryptering bruger aldrig adgangskoden direkte. HotXLS beregner først en itereret hash: Den indledende hash (digest) er SHA-512 over adgangskodesaltet sat sammen med adgangskodens UTF-16LE-bytes, og derefter hashes resultatet igen spinCount gange, hvor hver runde sætter den 32-bit little-endian iterations-tæller foran den forrige hash. Med Excels standard spin-antal på 100.000 er det et hundrede tusinde serielle SHA-512-kald pr. adgangskodeforsøg, og det er netop hele pointen. Spin-antallet er en brute-force dæmper: Det koster en legitim kalder et par millisekunder én gang, men koster en ordbogsangriber de samme par millisekunder for hvert enkelt gæt

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

Den færdige hash er stadig ikke en nøgle. Tre forskellige nøgler udledes fra den ved at hashe den endnu en gang med en fastgjort 8-byte bloknøgle tilføjet, én konstant til hvert formål: FE A7 D2 76 3B 4B 9E 79 til dekryptering af verifikatorinputtet, D7 AA 0F 6D 30 61 34 4E til verifikatorhashen og 14 6E 0B E7 AB AC D0 D6 til udpakning af selve pakkenøglen. Hvert SHA-512-resultat afkortes til den angivne nøglelængde og udfyldes i henhold til [MS-OFFCRYPTO] med 0x36-bytes i det teoretiske tilfælde, hvor hashen er kortere end nøglen. Den samme 0x36-udfyldningsregel gælder, når adgangskodesaltet udvides til blokstørrelse for at blive brugt som CBC-initialiseringsvektor (IV)

Verificering af adgangskode og fælden med afkortning af saltSize

HotXLS verificerer adgangskoden, før den rører ved pakken, ved at bruge verifikatorparret fra beskrivelsen. Den dekrypterer encryptedVerifierHashInput med den første udledte nøgle, hashes resultatet med SHA-512, dekrypterer encryptedVerifierHashValue med den anden udledte nøgle og sammenligner de to hash-værdier byte for byte. En uoverensstemmelse betyder, at adgangskoden er forkert, hvilket rapporteres som et særskilt resultat frem for en beskadiget arbejdsbog, og afgørende betyder det, at pakkekroppen aldrig dekrypteres med en forkert nøgle, så der er intet scenarie, hvor en forkert adgangskode producerer tilsyneladende rigtige, men korrupte data

Der er en specifikationsdetalje her, som er nem at tage fejl af. [MS-OFFCRYPTO] §2.3.4.13 definerer verifikatoren som saltSize bytes af tilfældige data, hvor saltSize er længden på nøglekrypteringens salt, not the krypteringsblokstørrelsen. Da AES-CBC-ciphertekst er blokjusteret, returneres det dekrypterede verifikatorinput udfyldt til et multiplum af 16 bytes, og det skal afkortes tilbage til saltSize før hashing. Excel altid skriver saltSize lig med blockSize, begge 16, så en implementering, der springer afkortningen over, består enhver test mod reelt Excel-output, men fejler på den første fil fra en producent, der har valgt en anden saltlængde. HotXLS afkorter til saltlængden, fordi det er det, specifikationen rent faktisk foreskriver, og at de to værdier er ens i praksis, er et tilfælde, ikke en kontrakt

Hvordan dekrypteres EncryptedPackage?

Strømmen EncryptedPackage begynder med en 8-byte little-endian klartekststørrelse efterfulgt af cipherteksten i 4096-byte segmenter, og HotXLS dekrypterer den segment for segment med en ny IV pr. segment. Selve pakkenøglen er ikke udledt af adgangskoden: Det er en tilfældig mellemliggende nøgle, som skriveren krypterede ind i encryptedKeyValue, og HotXLS pakker den ud med den tredje udledte nøgle og afkorter den til den nøglelængde, der er angivet af keyData. Hvert segments IV er SHA-512 over keyData-saltet sat sammen med det 32-bit little-endian segmentindeks, afkortet til blokstørrelsen. Denne konstruktion betyder, at ethvert 4096-byte segment kan dekrypteres uafhængigt, hvilket principielt gør formatet velegnet til vilkårlig adgang (random access), selvom HotXLS dekrypterer hele pakken til hukommelsen og overdrager de resulterende ZIP-bytes til sin normale XLSX-loader

Den angivne klartekststørrelse udfører det sidste stykke arbejde. AES-CBC-output er blokjusteret, så det sidste segment bærer op til 15 bytes udfyldning, som ikke er en del af dokumentet; den dekryptererede buffer afkortes til størrelsespræfikset, og resultatet er præcis den .xlsx-ZIP-fil, som Excel krypterede. HotXLS validerer præfikset mod den faktiske strømlængde før dekryptering, så et afkortet upload eller et manipuleret størrelsesfelt fejler rent i stedet for at løbe over

Fejlrapportering og ærlige grænser

Fejltilstandene holdes bevidst adskilt. En forkert adgangskode udløser en undtagelse med en eksplicit besked om forkert adgangskode, drevet af verifikatoruoverensstemmelsen, så en brugergrænseflade kan bede brugeren om at prøve igen. En CFB-beholder, hvis beskrivelse angiver algoritmer uden for det understøttede sæt — alt andet end AES med CBC-kædning og SHA-512-hashing i en Agile-beskrivelse, eller en beholder, der hverken er Standard eller Agile — udløser en anden undtagelse, der identificerer skemaet som understøttet. De to må aldrig forveksles: At prøve en adgangskode igen mod et ikke-understøttet skema spilder brugerens tid, og at rapportere en forkert adgangskode som en formatfejl sender dit supportteam i den forkerte retning

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;

Grænserne er værd at nævne klart. HotXLS læser Agile-beskrivelser, der angiver AES i CBC-tilstand med SHA-512, hvilket dækker det, som Excel 2010 til Excel 365 rent faktisk skriver, i alle tre nøglestørrelser. Beskrivelser, der angiver andre krypteringer eller hash-algoritmer, afvises frem for at blive gættet, og certifikatbaserede nøglekrypteringer konsulteres ikke, kun adgangskodenøglekrypteringen. På skrivesiden producerer HotXLS i øjeblikket Standard-kryptering frem for Agile, en forskel der har betydning, hvis efterfølgende værktøjer inspicerer skemaet; detaljerne findes i artiklen om oprettelse af AES-beskyttet XLSX-output

Adgangskodebeskyttede uploads ophører med at være et særtilfælde, når loaderen behandler kryptering som en del af filformatet frem for en undtagelse fra det. Indgangspunktet OpenEncrypted, den SHA-512-spin-count-udledning samt den segmenterede AES-CBC-pipeline, der er beskrevet her, leveres som en del af HotXLS Delphi Excel Component sammen med resten af dens indfødte XLS- og XLSX-læse- og skrivemotor til Delphi og C++Builder