Du skriver en arbejdsbog, krypterer den med en adgangskode, overdrager filen til en kollega, og kollegaen åbner den i Excel. Excel beder om adgangskoden. Kollegaen indtaster den, og Excel accepterer den. Indtil videre ser krypteringen korrekt ud. Derefter viser Excel en dialogboks, der siger, at filen is beskadiget og ikke kan åbnes, eller også åbnes den til et ark fyldt med meningsløse celler. Adgangskoden var rigtig. Filen er alligevel i stykker. Dette er det absolut mest forvirrende fejlmønster i Office-kryptering, fordi den del, der fortæller dig, at adgangskoden er rigtig, og den del, der indeholder dine data, er beskyttet af to forskellige handlinger. At få den ene rigtig garanterer ikke den anden
Begge fejl, der beskrives her, havde netop denne form. I begge tilfælde bestod verifikatoren (verifier), mens pakkekroppen (body) fejlede, hvilket sender dig på jagt efter en fejl i adgangskoden eller nøgleudledningen, som slet ikke er der. Den egentlige fejl lå længere nede i arbejdsgangen — i den måde, pakkens bytes blev transformeret på. De to fejl er uafhængige — den ene i AES-stien og den anden i RC4-stien — men de deler et fejlfindingsproblem, så det er værd at forstå, hvorfor et delvist korrekt resultat er det sværeste at tyde
Hvorfor en godkendt adgangskode intet beviser om pakkekroppen
Det format, som moderne krypteret XLSX anvender, er ECMA-376 Standard Encryption, og det gemmer to krypterede elementer side om side. Det ene er EncryptionVerifier: En lille blok, der indeholder en tilfældig værdi samt hashen af denne værdi, krypteret med den nøgle, der er udledt af adgangskoden. Det andet er EncryptedPackage: Hele arbejdsbogens zip-beholder, krypteret med samme nøgle. Verifikatoren eksisterer, så en læser kan bekræfte en adgangskode, før den bruger ressourcer på flere megabytes pakkekrop. Dekrypter verifikatoren, hash den tilfældige værdi, sammenlign den med den gemte hash, og hvis de stemmer overens, er adgangskoden korrekt
Fælden er, at verifikatoren og pakken krypteres ved separate kald over separate buffere. En korrekt udledt nøgle vil dekryptere verifikatoren korrekt, uanset hvad der sker med pakken bagefter. Så hvis din nøgleudledning er rigtig, men din pakketransformation er forkert, Excel bekræfter adgangskoden ud fra verifikatoren og fejler derefter på pakkekroppen. Symptomet læses som "rigtig adgangskode, ødelagt fil", hvilket retter undersøgelsen mod adgangskodestien, som netop var den del, der aldrig var i stykker. Samme adskillelse gælder for det ældre RC4-tilfælde: Verifikatorhashen kontrolleres først, og en pakkekrop, der driver ud af synkronisering, lader stadig denne kontrol forblive intakt
Fejl et: AES i ECB, ikke CBC
[MS-OFFCRYPTO] §2.3.4.15 angiver, at Standard Encryption krypterer pakken med AES i Electronic Codebook-tilstand (ECB). Hver 16-byte blok i den udfyldte pakke krypteres uafhængigt med samme nøgle. Der er ingen kædedannelse (chaining) mellem blokke, og der er ingen initialiseringsvektor (IV). Dette er et usædvanligt valg efter moderne standarder, hvor ECB normalt undgås, men interoperabilitet er ikke stedet til at sætte spørgsmålstegn ved specifikationen. Excel dekrypterer pakken som ECB, så en producent skal kryptere den som ECB, ellers vil de to ikke stemme overens
Fejlen var, at pakken blev krypteret med AES i CBC-tilstand ved brug af en initialiseringsvektor med lutter nuller. Her er grunden til, at det næsten fungerer, og hvorfor "næsten" er det værste sted at lande. I CBC bliver den første klartekstblok XOR'et med IV'en før kryptering. Når IV'en består af lutter nuller, ændrer denne XOR intet, så den første blok af CBC-med-nul-IV producerer nøjagtig samme ciphertekst som ECB. Fra den anden blok og frem føder CBC den foregående ciphertekstblok ind i den næste, så hver blok efter den første afviger fra ECB
Læg nu det oven på strukturen. Pakkelayoutet placerer et 8-byte little-endian længdepræfiks allerførst, så de dele af filen, Excel kontrollerer tidligst, ligger i den første blok eller to. At en første blok tilfældigvis matcher, betyder, at den tidligste validering godkendes, mens hver efterfølgende blok dekrypteres til støj. Løsningen er ikke svær, når tilstanden først er identificeret: Krypter hver 16-byte blok med ECB og stop kædedannelsen. I motoren gennemgår XlsEncryptStdPackage den udfyldte buffer i 16-byte trin og kalder AESEncryptECB128Block på hver enkelt, hvilket er den samme primitiv, som allerede bruges til verifikatorblokkene. Kildekoden indeholder en kommentar ved løkken, som formulerer reglen klart: CBC med en nul-IV matcher kun ECB for den første blok, så resten af pakken ville blive dekrypteret til affald, og Excel ville afvise den
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;
Fejl to: RC4-gennøglingen driver ud af takt
Den ældre .xls-sti bruger RC4 CryptoAPI-skemaet, og dens regel er af en anden karakter. [MS-OFFCRYPTO] §2.3.6 angiver, at ciphersystemet gennøgles (re-keyed) ved hver 1024-byte blokgrænse. Strømmen er opdelt i blokke på 1024 bytes, en ny RC4-nøgle udledes for blok nummer 0, 1, 2 osv., og inden for hver blok forbruges nøglestrømmen (keystream) kontinuerligt fra byte til byte. To invarianter skal overholdes samtidigt: Gennøgle ved hver grænse, og forbrug nøglestrømmen uden huller i en blok. RC4 er et strøm-ciphersystem (stream cipher), så dets nøglestrøm er en enkelt ordnet sekvens; den n'te byte, du trækker, bestemmes af, hvor mange bytes du har trukket før den. Dekryptering er den samme XOR mod samme sekvens, hvilket betyder, at producent og forbruger skal trække nøjagtig de samme bytes på nøjagtig de samme positioner
Dette er hele vanskeligheden. Et strøm-ciphersystem har ingen resynkronisering. Hvis du spilder en enkelt byte af nøglestrømmen, vil hver eneste byte efter den blive XOR'et med den forkerte nøglestrømbyte, og fejlen retter sig aldrig af sig selv; den fortsætter i kaskade til slutningen af blokken, og når den aktuelle position først er forkert, til hver eneste blok derefter. Fejlen her gjorde netop det. Bloktælleren startede fra en sentinel-værdi på minus én, og skip-rutinen antog, at tælleren allerede matchede den aktuelle blok. Startende fra denne sentinel gennøglede den og kørte en hel 1024-byte blok af nøglestrømmen, som aldrig burde være blevet forbrugt, og i processen gjorde den det resterende antal negativt. Fra det tidspunkt var dekrypteringsenheden en hel blok ude af fase. Verifikatoren, som blev kontrolleret før alt dette, blev stadig godkendt, så adgangskoden så rigtig ud, mens hver eneste datacelle kom ud som affald
Den korrigerede logik ligger i TXLSDecrypterRC4. Både Skip og Decrypt deler én løkke: Gennøgle kun, når den aktuelle position krydser ind i en ny blok, hvor blokindekset er positionen divideret med REKEY_BLOCK_SIZE (1024), og forbrug derefter op til resten af den aktuelle blok og ikke mere. MakeKey kaldes med blokindekset, aldrig med et forældet eller sentinel-indeks, og positionen rykker frem med det nøjagtige antal behandlede bytes, så Skip og Decrypt forbliver fasejusteret med producenten. Læren ligger i den mindste enhed: En enkelt spildt byte er ikke en lille fejl i et strøm-ciphersystem, det er et totalt tab af alt efterfølgende
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;
Interoperabilitet med en fastfrossen specifikation skal matche på byten
Begge fejl reducerer til det samme grundprincip, og det er værd at nævne for sig selv, fordi det ændrer på, hvordan du afvejer designvalg. Når modtageren af dit output er et fastlåst, eksternt program, du ikke kan ændre, er cipher-tilstanden og gennøglingstakten ikke implementeringsdetaljer, du kan vælge at optimere eller forenkle. De er en del af overførselsoverenskomsten. Excel vil dekryptere med ECB og gennøgle ved 1024-byte grænser, uanset om disse valg behager dig eller ej, og din eneste opgave er at producere bytes, der dekrypterer til originalen under netop den fremgangsmåde. En tilstand, der er mere moderne, en IV der virker harmløs, en tæller der starter, hvor det føles naturligt — ethvert af disse valg er en defekt i det øjeblik, det afviger fra, hvad læseren forventer. Interoperabilitet mod en fastfrossen specifikation er ikke omtrentlig. Det er byte-præcist, ellers er det i stykker
Dette er også grunden til, at verifikatoren alene er en dårlig kontrol. Den fortæller dig, at nøgleudledningen fungerer, hvilket er nødvendigt, men langt fra tilstrækkeligt. En test, der kun åbner en krypteret fil og bekræfter, at adgangskoden godkendes, vil rapportere succes, mens pakkekroppen er ulæselig. En rigtig test dekrypterer pakken og sammenligner de genskabte bytes med det oprindelige input, eller sender en arbejdsbog gennem kryptering og dekryptering og læser cellerne tilbage. Verifikatoren beviser adgangskoden — kun pakkekroppen beviser krypteringen
Den understøttede måde at læse og skrive beskyttede arbejdsbøger på
Den offentlige grænseflade er lille. For at skrive en adgangskodebeskyttet moderne arbejdsbog, skal du udfylde eller åbne en TXLSXWorkbook og kalde SaveAsEncrypted med et filnavn og en adgangskode; den serialiserer arbejdsbogen og kører standardkrypterings-pipelinen, som den første rettelse korrigerede, og returnerer 1 ved succes. For at læse, kalder du CanReadEncrypted for at teste, om en fil is en krypteret Compound File-beholder, og forgrenes derefter: OpenEncrypted håndterer den krypterede sti og falder tilbage på Open for almindelige filer, og Open med en adgangskode er tilgængelig direkte. Tilstandshåndteringen og gennøglerløkken beskrevet ovenfor ligger under disse kald — du leverer adgangskoden og filnavnet, og motoren matcher specifikationen på dine vegne
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å det beskyttede output, EncryptionInfo-strømmen, verifikatorblokkene og pakkelayoutet dækkes i vores gennemgang af AES-beskyttet XLSX-output. For det særskilte qvortment om låsning på arkniveau og hvordan beskyttelse interagerer med sideopsætning og udskrivning, se artiklen om beskyttelse, sideopsætning og udskrivning. Begge bygger på den krypteringssti, der er beskrevet her, som leveres som en del af HotXLS spreadsheet component til Delphi og C++Builder sammen med de læse-, skrive- og gengivelses-API'er, der dækkes andre steder på denne blog