Teknisk artikel

Varför Excel avvisar din krypterade arbetsbok: ECB och RC4

Du skriver en arbetsbok, krypterar den med ett lösenord, lämnar filen till en kollega och kollegan öppnar den i Excel. Excel frågar efter lösenordet. Kollegan skriver in det och Excel accepterar det. Så långt ser krypteringen korrekt ut. Sedan visar Excel en dialogruta som säger att filen är skadad och inte kan öppnas, eller så öppnas den till ett kalkylblad med meningslösa celler. Lösenordet var rätt. Filen är ändå trasig. Detta är det absolut mest förvirrande felläget i Office-kryptering, eftersom den del som berättar att lösenordet är rätt och den del som innehåller dina data skyddas av två olika operationer, och att få den ena rätt garanterar ingenting för den andra

Båda buggarna som beskrivs här hade exakt denna form. I båda fallen godkändes verifieraren men inte paketkroppen, vilket får dig att leta efter fel i lösenordet eller nyckelgenereringen som inte finns där. Det verkliga felet låg nedströms, i hur paketets byte transformerades. De två felen är oberoende av varandra, ett i AES-sökvägen och ett i RC4-sökvägen, men de delar ett diagnosproblem, så det är värt att se varför ett halvt korrekt resultat är det svåraste att tyda

Varför ett godkänt lösenord ingenting bevisar om paketkroppen

Formatet som modern krypterad XLSX använder är ECMA-376 Standard Encryption, och det lagrar två krypterade saker sida vid sida. Det ena är EncryptionVerifier: ett litet block som innehåller ett slumpmässigt värde och hashen av det värdet, krypterat med nyckeln som härletts från lösenordet. Det andra är EncryptedPackage: hela zip-behållaren för arbetsboken, krypterad med samma nyckel. Verifieraren finns till för att en läsare ska kunna bekräfta ett lösenord innan den lägger energi på megabyte av paketkropp. Dekryptera verifieraren, hasha det slumpmässiga värdet, compare it to the stored hash, and if they match the password is correct

Fällan är att verifieraren och paketet krypteras genom separata anrop över separata buffertar. En nyckel som härleds korrekt kommer att dekryptera verifieraren korrekt oavsett vad som händer med paketet efteråt. Så om din nyckelgenerering är rätt men din pakettransformering är fel, bekräftar Excel lösenordet från verifieraren och misslyckas sedan på paketkroppen. Symptomet tolkas som ”rätt lösenord, trasig fil”, vilket riktar utredningen mot lösenordssökvägen, vilket var den enda del som aldrig var trasig. Samma separation styr det äldre RC4-fallet: verifierarens hash kontrolleras först, och en kropp som driver ur synk lämnar fortfarande den kontrollen intakt

Fel ett: AES i ECB, inte CBC

[MS-OFFCRYPTO] §2.3.4.15 anger att Standard Encryption krypterar paketet med AES i Electronic Codebook-läge. Varje 16-bytes block i det utfyllda paketet krypteras oberoende med samma nyckel. Det finns ingen kedjekoppling mellan blocken och det finns ingen initieringsvektor. Detta är ett ovanligt val med moderna mått mätt, där ECB normalt undviks, men samverkan är inte en plats för att ifrågasätta specifikationen. Excel dekrypterar paketet som ECB, så en producent måste kryptera det som ECB annars kommer de två inte överens

Felet var att paketet krypterades med AES i CBC-läge med en initieringsvektor med bara nollor. Här är varför det nästan fungerar, och varför nästan är det sämsta stället att hamna på. I CBC XOR-as det första plaintext-blocket med IV:n före kryptering. När IV:n består av enbart nollor ändrar den XOR-operationen ingenting, så det första blocket av CBC-med-noll-IV ger exakt samma chiffertext som ECB. Från det andra blocket och framåt matar CBC in föregående chiffertextblock i nästa, så varje block efter det första skiljer sig från ECB

Lägg nu det över strukturen. Paketlayouten placerar ett 8-byte längdprefix i little-endian allra först, så de delar av filen som Excel kontrollerar tidigast sitter i det första blocket eller två. Att ett första block råkar matcha innebär att den tidigaste valideringen godkänns medan varje senare block dekrypteras till brus. Lösningen är inte komplicerad när läget väl har identifierats: kryptera varje 16-bytes block med ECB och sluta kedjekoppla. I motorn går XlsEncryptStdPackage walks the padded buffer in 16-byte steps and calls AESEncryptECB128Block on each one, which is the same primitive already used for the verifier blocks. Källkoden innehåller en kommentar vid loopen som tydligt anger regeln: CBC med en noll-IV matchar endast ECB för det första blocket, så resten av paketet skulle dekrypteras till skräp och Excel skulle avvisa det

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted serialiserar arbetsboken och kör sedan
    // ECMA-376 Standard Encryption-pipelinen: AES-128 ECB över
    // paketet enligt [MS-OFFCRYPTO] 2.3.4.15. Returnerar 1 vid framgång.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Fel två: RC4-omnycklingen kommer ur fas

Den äldre .xls-sökvägen använder RC4 CryptoAPI-schemat, och dess regel är av ett annat slag. [MS-OFFCRYPTO] §2.3.6 anger att chiffreringen nycklas om vid varje 1024-bytes blockgräns. Strömmen är uppdelad i block om 1024 byte, en ny RC4-nyckel härleds för block nummer 0, 1, 2 och så vidare, och inom varje block konsumeras keystream kontinuerligt från byte till byte. Två invarianter måste hållas samman: nyckla om vid varje gräns och konsumera keystream utan luckor inuti ett block. RC4 är ett strömchiffer, så dess keystream är en enda ordnad sekvens; den n-te byten du drar bestäms av hur många byte du har dragit före den. Dekryptering är samma XOR mot samma sekvens, vilket innebär att producent och konsument måste dra exakt samma byte vid exakt samma positioner

Felet här gjorde exakt det. Blockräknaren startade från ett vaktvärde (sentinel) på minus ett, och hopprutinen (skip routine) antog att räknaren redan matchade det aktuella blocket. Med utgångspunkt från det vaktvärdet nycklade den om och körde ett helt 1024-bytes block av keystream som aldrig borde ha konsumerats, och i processen drev den det återstående antalet till negativt. Från den tidpunkten var dekrypteraren ett helt block ur fas. Verifieraren, som kontrollerades före allt detta, godkändes fortfarande, så lösenordet såg rätt ut medan varje datacell kom ut som skräp

Den korrigerade logiken finns i TXLSDecrypterRC4. Både Skip och Decrypt delar en loop: nyckla om endast när den löpande positionen går över i ett nytt block, där blockindexet är positionen dividerad med REKEY_BLOCK_SIZE (1024), konsumera sedan upp till resten av det aktuella blocket och inte mer. MakeKey anropas med blockindexet, aldrig med ett föråldrat index eller vaktindex, och positionen flyttas framåt med exakt det antal byte som bearbetats så dat Skip och Decrypt förblir fasinriktade med producenten. Lärdomen ligger i den minsta enheten: en enda bortkastad byte är inte ett litet fel i ett strömchiffer, det är en total förlust av allt nedströms

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    // CanReadEncrypted kontrollerar signaturen för Compound File (OLE2) så att
    // du kan förgrenas innan du försöker med en normal Open. OpenEncrypted
    // dirigerar vanliga filer till Open och hanterar den krypterade behållaren.
    if Book.CanReadEncrypted('legacy.xls') then
      Book.OpenEncrypted('legacy.xls', 'S3cret!')
    else
      Book.Open('legacy.xls');
    // read cells here
  finally
    Book.Free;
  end;
end;

Samverkan med en fryst specifikation kräver exakt matchning på bytenivå

Båda felen kokar ner till samma grundprincip, och det är värt att slå fast i sig självt eftersom det förändrar hur du väger designval. När konsumenten av dina utdata är ett fast externt program som du inte kan ändra, är chifferläget och omnycklingstakten inte implementeringsdetaljer som du kan optimera eller förenkla. De är en del av kontraktet. Excel kommer att dekryptera med ECB och nyckla om på 1024-bytes blockgränser oavsett om dessa val behagar dig eller inte, och din enda uppgift är att producera byte som dekrypteras till originalet under exakt det förfarandet. Ett läge som är modernare, en IV som verkar harmlös, en räknare som startar där det känns naturligt; alla dessa är defekter i samma ögonblick som de avviker från vad läsaren förväntar sig. Samverkan mot en fryst specifikation är inte ungefärlig. Den är byte-exakt eller så är den trasig

Detta är också anledningen till att verifieraren i sig är ett dåligt röktest. Den talar om för dig att nyckelgenereringen fungerar, vilket är nödvändigt men långt ifrån tillräckligt. Ett test som bara öppnar en krypterad fil och bekräftar att lösenordet godkänns kommer att rapportera framgång medan paketkroppen är oläslig. Ett riktigt test dekrypterar paketet och jämför de återställda byten med originalindatan, eller kör en arbetsbok fram och tillbaka genom kryptering och dekryptering och läser tillbaka celler. Verifieraren bevisar lösenordet; endast paketkroppen bevisar krypteringen

Det stödda sättet att läsa och skriva skyddade arbetsböcker

Det offentliga gränssnittet är litet. För att skriva en lösenordsskyddad modern arbetsbok fyller du eller öppnar en TXLSXWorkbook och anropar SaveAsEncrypted med ett filnamn och ett lösenord; det serialiserar arbetsboken och kör Standard Encryption-pipelinen som den första rättelsen korrigerade, och returnerar 1 vid framgång. För att läsa anropar du CanReadEncrypted för att testa om en fil är en krypterad Compound File-behållare, och förgrenar sedan: OpenEncrypted hanar den krypterade sökvägen och faller tillbaka på Open för vanliga filer, och Open med ett lösenord är tillgängligt direkt. Fellägeshanteringen och omnycklingsloopen som beskrivs ovan ligger under dessa anrop; du tillhandahåller lösenordet och filnamnet och motorn matchar specifikationen å dina vägnar

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // Öppna igen på konsumentsidan
    Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
  finally
    Book.Free;
  end;
end;

Utformningen av de skyddade utdata, strömmen EncryptionInfo, verifierarblocken och paketlayouten behandlas i vår genomgång av AES-skyddade XLSX-utdata. För den separata frågan om låsning på bladnivå och hur skydd samverkar med sidinställningar och utskrift, se artikeln om skydd, sidinställningar och utskrift. Båda bygger på krypteringssökvägen som beskrivs här, som levereras som en del av HotXLS spreadsheet-komponent för Delphi och C++Builder tillsammans med läs-, skriv- och renderar-API:er som behandlas på andra ställen på den här bloggen