Du skriver en arbetsbok, krypterar den med ett lösenord, lämnar filen till en kollega, och kollegan öppnar den i Excel. Excel ber om 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 blad med meningslösa celler. Lösenordet var rätt. Filen är ändå trasig. Detta är det enskilt mest förvirrande felläget i Office-kryptering, eftersom delen som talar om för dig att lösenordet är rätt och delen som innehåller dina data skyddas av två olika operationer, och att få den ena rätt garanterar inte att den andra är det
Båda buggarna som beskrivs här hade exakt denna form. I båda fallen godkändes verifieraren men inte kroppen, vilket får dig att leta efter en lösenords- eller nyckelhärledningsbugg som inte finns där. Det verkliga felet fanns nedströms, i hur paketets bytes transformerades. De två felen är oberoende, ett i AES-sökvägen och ett i RC4-sökvägen, som delar ett diagnosproblem, så det är värt att förstå varför ett halvkorrekt resultat är det svåraste att tolka
Varför ett godkänt lösenord inte bevisar någonting om kroppen
Formatet som den moderna krypterade XLSX använder är ECMA-376 Standard Encryption, och det lagrar två krypterade saker sida vid sida. Den ena är EncryptionVerifier: ett litet block som innehåller ett slumpmässigt värde och hash-värdet för detta värde, krypterat med nyckeln härledd från lösenordet. Den andra är EncryptedPackage: hela zip-behållaren för arbetsboken, krypterad med samma nyckel. Verifieraren finns för att en läsare ska kunna bekräfta ett lösenord innan den lägger resurser på megabyte av kroppsinnehåll. Dekryptera verifieraren, hasha det slumpmässiga värdet, jämför det med det lagrade hash-värdet, och om de matchar är lösenordet korrekt
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 nyckelhärledning är rätt men din pakettransformering är fel, Excel bekräftar lösenordet från verifieraren och misslyckas sedan på kroppen. Symptomet läses som "rätt lösenord, trasig fil", vilket riktar undersökningen mot lösenordssökvägen, vilken är 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
Bugg ett: AES i ECB, inte CBC
[MS-OFFCRYPTO] §2.3.4.15 specificerar att Standard Encryption krypterar paketet med AES i Electronic Codebook-läge. Varje 16-bytes block av det utfyllda paketet krypteras oberoende med samma nyckel. Det finns ingen kedja mellan block och det finns ingen initieringsvektor. Detta är ett ovanligt val med moderna mått mätt, där ECB vanligtvis undviks, men kompatibilitet är inte en plats att ifrågasätta specifikationen. Excel dekrypterar paketet som ECB, så en producent måste kryptera det som ECB annars kommer de två inte att stämma överens
Buggen var att paketet krypterades med AES i CBC-läge med en initieringsvektor med idel nollor. Här är anledningen till att det nästan fungerar, och varför nästan är det sämsta stället att landa på. I CBC XOR-operation görs det första klartextblocket med IV:n före kryptering. När IV:n är idel nollor ändrar den XOR-operationen ingenting, så det första blocket av CBC-med-noll-IV producerar exakt samma chiffertext som ECB. Från det andra blocket och framåt matar CBC det föregående chiffertextblocket in i nästa, så varje block efter det första avviker från ECB
Lägg nu detta över strukturen. Paketlayouten placerar ett 8-bytes längdprefix med little-endian i början, så de delar av filen som Excel kontrollerar tidigast sitter i det första eller de två första blocken. Ett första block som råkar matcha innebär att den tidigaste valideringen godkänns medan varje senare block dekrypteras till brus. Lösningen är inte subtil när läget väl har identifierats: kryptera varje 16-bytes block med ECB och sluta kedja. I motorn går XlsEncryptStdPackage igenom den utfyllda bufferten i 16-bytes steg och anropar AESEncryptECB128Block på varje, vilket är samma primitiv som redan används för verifierarblocken. Källkoden bär en kommentar vid loopen som anger regeln tydligt: 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 serializes the workbook, then runs the
// ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
// package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
raise Exception.Create('Encryption failed');
finally
Book.Free;
end;
end;
Bugg två: RC4-omnycklingen driver 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 specificerar att chiffret nycklas om vid varje 1024-bytes blockgräns. Strömmen är uppdelad i block om 1024 bytes, en ny RC4-nyckel härleds för block nummer 0, 1, 2 och så vidare, och inom varje block konsumeras nyckelströmmen kontinuerligt från byte till byte. Två invarianter måste hållas samman: nyckla om vid varje gräns, och konsumera nyckelströmmen utan luckor inuti ett block. RC4 är ett strömchiffer, så dess nyckelström är en enda ordnad sekvens; den n-te byten du ritar bestäms av hur många bytes du har ritat före den. Dekryptering är samma XOR mot samma sekvens, vilket innebär att producent och konsument måste rita exakt samma bytes på exakt samma positioner
Detta är hela svårigheten. Ett strömchiffer jag ingen resynkronisering. Om du slösar bort en enda byte av nyckelströmmen blir varje byte efter den XOR-opererad mot fel nyckelström-byte, och felet korrigeras aldrig; det sprider sig till slutet av blocket och, när körpositionen väl är fel, till varje block efter det. Buggen här gjorde exakt det. Blockräknaren startade från ett vaktpostvärde på minus ett, och hopprutinen antog att räknaren redan matchade det aktuella blocket. Med utgångspunkt från den vaktposten nycklade den om och körde ett helt 1024-bytes block av nyckelström som aldrig borde ha konsumerats, och under processen drev den resterande räkningen negativ. 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 körpositionen korsar in 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 vaktpostindex, och positionen flyttas framåt med det exakta antalet bearbetade bytes så att Skip och Decrypt förblir fasjusterade med producenten. Lärdomen finns i den minsta enheten: en enda förlorad 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 checks the Compound File (OLE2) signature so
// you can branch before attempting a normal Open. OpenEncrypted
// routes plain files to Open and handles the encrypted container.
if Book.CanReadEncrypted('legacy.xls') then
Book.OpenEncrypted('legacy.xls', 'S3cret!')
else
Book.Open('legacy.xls');
// read cells here
finally
Book.Free;
end;
end;
Kompatibilitet med en frusen specifikation är matchning på byte-nivå
Båda buggarna kan reduceras till samma grundprincip, och den är värd att nämna för sig själv eftersom den ändrar hur du värderar designval. När mottagaren 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 trådkontraktet. Excel kommer att dekryptera med ECB och nyckla om på 1024-bytes gränser oavsett om dessa val behagar dig eller inte, och ditt enda jobb är att producera bytes som dekrypteras till originalet under exakt det förfarandet. Ett läge som är modernare, en IV som verkar ofarlig, en räknare som startar där det känns naturligt; alla dessa är en defekt i samma ögonblick som de avviker från vad läsaren förväntar sig. Kompatibilitet mot en frusen specifikation är inte ungefärlig. Den är byte-exakt eller så är den trasig
Det är därför verifieraren är ett dåligt röktest i sig själv. Den talar om för dig att nyckelhärledningen 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 fungerar kommer att rapportera framgång medan kroppen är oläsbar. Ett riktigt test dekrypterar paketet och jämför de återställda bytes med ursprungliga indata, eller kör en arbetsbok fram och tillbaka genom kryptering och dekryptering och läser cellerna tillbaka. Verifieraren bevisar lösenordet; endast kroppen 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, fyll i eller öppna en TXLSXWorkbook och anropa SaveAsEncrypted med ett filnamn och ett lösenord; det serialiserar arbetsboken och kör pipelinen för Standard Encryption som den första korrigeringen åtgärdade, och returnerar 1 vid framgång. För att läsa, anropa CanReadEncrypted för att testa om en fil är en krypterad Compound File-behållare, grena 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. Lägeshanteringen och omnycklingsloopen som beskrivs ovan ligger under dessa anrop; du anger 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');
// Reopen on the consumer side
Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
finally
Book.Free;
end;
end;
Formen på de skyddade utdata, EncryptionInfo-strömmen, verifierarblocken och paketlayouten beskrivs i vår genomgång av AES-skyddade XLSX-utdata. För den separata frågan om låsning på bladnivå och hur skydd samspelar med sidinställningar och utskrift, se artikeln om skydd, sidinställningar och utskrift. Båda bygger på krypteringssökvägen som beskrivs här, vilken levereras som en del av HotXLS spreadsheet component för Delphi och C++Builder tillsammans med de API:er för inläsning, skrivning och rendering som beskrivs på andra ställen i denna blogg