Teknisk artikel

Läs Agile-krypterade Excel-filer i Delphi med HotXLS

HotXLS läser Agile-krypterade Excel-filer, det lösenordsskydd som Excel 2010 och alla senare versioner tillämpar som standard, genom ett enda anrop: TXLSXWorkbook.OpenEncrypted. Komponenten tolkar XML-krypteringsbeskrivningen, härleder nycklar från lösenordet med en SHA-512 spin-count-hashkedja, verifierar lösenordet mot den krypterade verifieraren och dekrypterar sedan paketet i 4096-byte AES-CBC-segment. Ingen Excel-installation, ingen COM, ingen extern kryptografisk DLL är inblandad

Den här artikeln behandlar specifikt lässidan av Agile-kryptering. Två närliggande problem har sina egna artiklar: samverkan med de äldre RC4- och XOR-schemana i gamla BIFF .xls-filer behandlas i artikeln om ECB- och RC4-samverkan, och att skapa lösenordsskyddade arbetsböcker med ECMA-376 Standard Encryption behandlas i artikeln om AES-skyddad XLSX-utdata. Här finns filen redan, någon annan har krypterat den, och ditt jobb är att öppna den

Scenariot som tvingar fram frågan är välkänt för alla som driver en dokumentpipeline. En importtjänst på serversidan tar emot uppladdade arbetsböcker; det finns ingen Excel installerad på datorn och kommer aldrig att finnas; och en morgon laddar en kund upp en helt vanlig .xlsx som ZIP-läsaren avvisar eftersom det inte alls är en ZIP-fil. Kunden sparade den med ett lösenord. Från det ögonblicket måste din inläsare antingen förstå [MS-OFFCRYPTO] eller så skickar den tillbaka filen till en användare som, ur sin synvinkel, inte gjorde något ovanligt

Vad är Agile-kryptering i en Excel-fil?

Agile-kryptering är det lösenordsskyddssystem som definieras i [MS-OFFCRYPTO] §2.3.4.10 till §2.3.4.15, och det är vad Excel 2010 och later skriver när en arbetsbok sparas med ett lösenord. Den krypterade filen är inte längre ett ZIP-paket. Det är en OLE Compound File Binary (CFB)-behållare som innehåller två strömmar: EncryptionInfo, som beskriver hur krypteringen utfördes, och EncryptedPackage, som är den verkliga .xlsx-ZIP-filen krypterad som en ogenomskinlig blob. CFB-signaturen (D0 CF 11 E0 A1 B1 1A E1) är samma magi som äldre BIFF .xls-filer bär på, vilket är anledningen till att en omdöpt eller krypterad fil inte kan klassificeras enbart efter filändelse

Det som skiljer Agile från dess föregångare är att EncryptionInfo är självbeskrivande. Efter ett 8-byte versionsprefix, med både huvud- och underversion satt till 4, är strömmen en XML-beskrivare i UTF-8. Ett keyData-element deklarerar chiffreringen (AES), kedjeläget (ChainingModeCBC), hashfunktionen (SHA512), nyckellängden i bitar, blockstorleken och ett Base64-salt. Ett lösenordsbaserat keyEncryptor-element bär sitt eget salt, spinCount och tre Base64-laddningar: encryptedVerifierHashInput, encryptedVerifierHashValue och encryptedKeyValue. Excel skriver AES-256 med ett spinn-antal på 100 000, men beskrivaren tillåts deklarera AES-128 eller AES-192, och HotXLS respekterar vad keyBits anger istället för att förutsätta 256

En startpunkt för oskyddade, Standard- och Agile-arbetsböcker

TXLSXWorkbook.OpenEncrypted hanar alla tre tillstånd en anropare kan stöta på: vanlig ZIP, Standard-krypterad och Agile-krypterad, så uppladdningshanterare behöver inte klassificera filer innan de läser in dem. Metoden sniffar först på filen: om det inte finns någon CFB-signatur lämnar den över till den normala Open-sökvägen och lösenordet ignoreras helt enkelt. Om filen är en CFB-behållare försöker den med ECMA-376 Standard Encryption först och, när versionssignaturen i EncryptionInfo är Agile 4.4, skickar den vidare till Agile-pipelinen. Returvärdet är 1 vid framgång, samma kontrakt som för Open

var
  Wb: TXLSXWorkbook;
begin
  Wb := TXLSXWorkbook.Create;
  try
    // Fungerar på samma sätt för vanliga .xlsx, Standard-krypterade och
    // Agile-krypterade filer
    if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
      Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
  finally
    Wb.Free;
  end;
end;

Reservalternativet för okrypterad indata spelar större roll än det verkar. En batch-importerare som alltid anropar OpenEncrypted behöver ingen förgrening vid anropsstället: filer som aldrig varit skyddade läses in exakt som tidigare, och filer som anländer krypterade dekrypteras på plats och matas sedan till den vanliga ZIP-inläsaren som en ström i minnet. Det finns bara en kodväg att testa, inte tre

Hur blir ett lösenord en AES-nyckel?

Agile-kryptering använder aldrig lösenordet direkt. HotXLS beräknar först en upprepad hash: den första digesten är SHA-512 över lösenordssaltet sammanfogat med lösenordets UTF-16LE-byte, och sedan hashas digesten om spinCount gånger, där varje omgång lägger till den 32-bitars lilla-endian-iterationsräknaren före föregående digest. Med Excels standard-spinn-antal på 100 000 blir det etthundratusen seriella SHA-512-anrop per lösenordsförsök, och det är hela poängen. Spinn-antalet är ett brute-force-hinder: det kostar en legitim anropare några millisekunder en gång, och kostar en ordboksangripare samma millisekunder för varje enskild gissning

// [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);            // iterationsräknare, little-endian
    Move(Result[0], buf[4], 64);   // föregående digest
    Result := XlsSHA512(buf);
  end;
end;

Den spunnit hashen är fortfarande inte en nyckel. Tre olika nycklar härleds från den genom att hasha den en gång till med en fast 8-byte blocknyckel tillagd, en konstant per syfte: FE A7 D2 76 3B 4B 9E 79 för att dekryptera verifierarens indata, D7 AA 0F 6D 30 61 34 4E för verifierarens hash, och 14 6E 0B E7 AB AC D0 D6 för att packa upp den faktiska paketnyckeln. Varje SHA-512-resultat till den deklarerade nyckellängden och, enligt [MS-OFFCRYPTO], fylls med 0x36-byte i det teoretiska fallet där hashen är kortare än nyckeln. Samma 0x36-utfyllnadsregel gäller när lösenordssaltet utökas till blockstorlek för användning som CBC-initieringsvektor

Lösenordsverifiering och saltSize-trunkeringsfällan

HotXLS verifierar lösenordet innan det rör vid paketet, med hjälp av verifierarparet från beskrivaren. Den dekrypterar encryptedVerifierHashInput med den första härledda nyckeln, hashar resultatet med SHA-512, dekrypterar encryptedVerifierHashValue med den andra härledda nyckeln och jämför de två digesterna byte för byte. En avvikelse innebär att lösenordet är felaktigt, vilket rapporteras som ett separat resultat snarare än en förvrängd arbetsbok, och kritiskt nog innebär det att paketkroppen aldrig dekrypteras med en felaktig nyckel, så det finns inget scenario där ett felaktigt lösenord ger trovärdigt utseende men korrupta data

Det finns en specifikationsdetalj här som är lätt att missförstå. [MS-OFFCRYPTO] §2.3.4.13 definierar verifieraren som saltSize byte av slumpmässig data, där saltSize är längden på nyckelkrypterarens salt, inte chiffreringens blockstorlek. Eftersom AES-CBC-chiffertext är blockjusterad kommer den dekrypterade verifierarindatan tillbaka utfylld till en multipel av 16 byte, och den måste trunkeras tillbaka till saltSize före hashing. Excel skriver alltid saltSize lika med blockSize, båda 16, så en implementering som hoppar över trunkeringen klarar varje test mot verkliga Excel-utdata och misslyckas sedan på den första filen från en producent som valt en annan saltlängd. HotXLS trunkerar till saltlängden eftersom det är vad specifikationen faktiskt anger, och att de kvärdena överensstämmer i praktiken är en tillfällighet, inte ett kontrakt

Hur dekrypteras EncryptedPackage?

Strömmen EncryptedPackage börjar med en 8-byte plaintext-storlek i little-endian, följt av chiffertexten i 4096-byte-segment, och HotXLS dekrypterar den segment för segment med en ny IV per segment. Paketnyckeln i sig är inte lösenordshärledd: det är en slumpmässig mellanliggande nyckel som skrivaren krypterade i encryptedKeyValue, och HotXLS packar upp den med den tredje härledda nyckeln, med trunkering till den nyckellängd som deklarerats av keyData. Varje segments IV är SHA-512 över keyData-saltet sammanfogat med det 32-bitars segmentindexet i little-endian, trunkerat till blockstorleken. Den konstruktionen gör att vilket 4096-byte-segment som helst kan dekrypteras oberoende av varandra, vilket i princip är det som gör formatet lämpligt för direktåtkomst (random access), även om HotXLS dekrypterar hela paketet till minnet och lämnar över de resulterande ZIP-byten till sin normala XLSX-inläsare

Den deklarerade plaintext-storleken gör det sista arbetet. AES-CBC-utdata är blockjusterad, så det sista segmentet bär på upp till 15 byte utfyllnad som inte är en del av dokumentet; den dekrypterade bufferten trunkeras till storleksprefixet, och resultatet är exakt den .xlsx-ZIP-fil som Excel krypterade. HotXLS validerar prefixet mot den faktiska strömlängden före dekryptering, så att en trunkerad uppladdning eller ett manipulerat storleksfält misslyckas rent istället för att spilla över

Felrapportering och ärliga gränser

Fellägena hålls medvetet isär. Ett felaktigt lösenord utlöser ett undantag med ett explicit felmeddelande om felaktigt lösenord, drivet av verifieraravvikelsen, så att ett gränssnitt kan uppmana användaren att försöka igen. En CFB-behållare vars beskrivare deklarerar algoritmer utanför den uppsättning som stöds — allt annat än AES med CBC-kedjekoppling och SHA-512-hashing i en Agile-beskrivare, eller en behållare som varken är Standard eller Agile — utlöser ett annat undantag som identifierar schemat som stöds inte. De två får aldrig sammanblandas: att försöka med ett lösenord igen mot ett schema som inte stöds slösar bort användarens tid, och att rapportera ett felaktigt lösenord som ett formatfel leder ditt supportteam i fel riktning

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
      // Utlöses för både ett felaktigt lösenord och ett schema som inte stöds;
      // E.Message anger vilket, så logga det ordagrant och
      // erbjuda endast lösenordsförsök igen för fallet med felaktigt lösenord
      RejectUpload(FileName, E.Message);
  end;
end;

Gränserna är värda att uttalas tydligt. HotXLS läser Agile-beskrivare som deklarerar AES i CBC-läge med SHA-512, vilket täcker vad Excel 2010 till Excel 365 faktiskt skriver, i alla tre nyckelstorlekar. Beskrivare som deklarerar andra chiffreringar eller hash-algoritmer avvisas snarare än att gissas, och certifikatbaserade nyckelkrypterare konsulteras inte, endast den lösenordsbaserade nyckelkrypteraren gör det. På skrivsidan producerar HotXLS för närvarande Standard Encryption snarare än Agile, en skillnad som spelar roll om efterföljande verktyg inspekterar schemat; detaljerna finns i artikeln om att skriva AES-skyddad XLSX-utdata

Lösenordsskyddade uppladdningar upphör att vara ett specialfall när inläsaren behandlar kryptering som en del av filformatet snarare än ett undantag till det. Startpunkten OpenEncrypted, SHA-512 spin-count-härledningen och den segmenterade AES-CBC-pipelinen som beskrivs här levereras som en del av HotXLS Delphi Excel Component, tillsammans med resten av dess inbyggda inläsnings- och skrivmotor för XLS och XLSX för Delphi och C++Builder