Du skriver en arbeidsbok, krypterer den med et passord, overleverer filen til en kollega, og kollegaen åpner den i Excel. Excel ber om passordet. Kollegaen taster det inn, og Excel godtar det. Så langt ser krypteringen riktig ut. Deretter viser Excel en dialogboks som sier at filen er korrupt og ikke kan åpnes, eller den åpnes til et ark med meningsløse celler. Passordet var riktig. Filen er ødelagt likevel. Dette er den mest forvirrende feilmodusen i Office-kryptering, fordi delen som forteller deg at passordet er riktig og delen som holder dataene dine beskyttes av to forskjellige operasjoner, og å få den ene riktig gir ingen garanti for den andre.
Begge feilene beskrevet her hadde akkurat denne formen. I hvert tilfelle var verifiseringen i orden, men kroppen var ikke det, noe som sender deg på jakt etter en passord- eller nøkkelutledningsfeil som ikke finnes. Den virkelige feilen lå nedstrøms, i hvordan pakkens byte ble transformert. De to feilene er uavhengige, den ene i AES-banen og den andre i RC4-banen, men de deler et diagnosespørsmål, så det er verdt å se hvorfor et delvis riktig resultat er det vanskeligste å tolke.
Hvorfor et godkjent passord ikke beviser noe om kroppen
Formatet den moderne krypterte XLSX bruker er ECMA-376 Standard Encryption, og det lagrer to krypterte ting side om side. Det ene er EncryptionVerifier: en liten blokk som holder en tilfeldig verdi og hashen til den verdien, kryptert med nøkkelen utledet fra passordet. Det andre er EncryptedPackage: hele zip-beholderen til arbeidsboken, kryptert med samme nøkkel. Verifiseringen eksisterer slik at en leser kan bekrefte et passord før den bruker ressurser på megabyte med data. Dekrypter verifiseringen, hash den tilfeldige verdien, sammenlign den med den lagrede hashen, og hvis de samsvarer er passordet riktig.
Fellen er at verifiseringen og pakken krypteres med separate kall over separate buffere. En nøkkel som er utledet riktig vil dekryptere verifiseringen riktig uansett hva som skjer med pakken etterpå. Så hvis nøkkelutledningen din er riktig, men pakketransformasjonen din er feil, bekrefter Excel passordet fra verifiseringen og feiler deretter på kroppen. Symptomet fremstår som "riktig passord, ødelagt fil", noe som peker etterforskningen mot passordbanen, som var den eneste delen som aldri var ødelagt. Den samme separasjonen gjelder for det eldre RC4-tilfellet: verifiseringshashen sjekkes først, og en kropp som driver ut av synk lar fortsatt denne sjekken forbli intakt.
Feil én: AES i ECB, ikke CBC
[MS-OFFCRYPTO] §2.3.4.15 spesifiserer at Standard Encryption krypterer pakken med AES i Electronic Codebook-modus. Hver 16-byte blokk av den utfylte pakken krypteres uavhengig med den samme nøkkelen. Det er ingen kjeding mellom blokker og ingen initialiseringsvektor. Dette er et uvanlig valg etter moderne standarder, der ECB normalt unngås, men interop er ikke et sted for å overprøve spesifikasjonen. Excel dekrypterer pakken som ECB, så en produsent må kryptere den som ECB ellers vil ikke de to være enige.
Feilen var at pakken ble kryptert med AES i CBC-modus ved bruk av en initialiseringsvektor med bare nuller. Her er grunnen til at det nesten fungerer, og hvorfor "nesten" er det verste stedet å ende opp. I CBC blir den første klartekstblokken XOR-et med IV-en før kryptering. Når IV-en er bare nuller, endrer ikke denne XOR-operasjonen noe, så den første blokken med CBC-med-null-IV produserer nøyaktig samme kryptotekst som ECB. Fra den andre blokken og utover mater CBC forrige kryptotekstblokk inn i den neste, slik at hver blokk etter den første avviker fra ECB.
Legg nå dette over på strukturen. Pakkeoppsettet plasserer et 8-byte lite-endian lengdeprefiks helt i starten, slik at delene av filen Excel sjekker tidligst sitter i de første par blokkene. En første blokk som tilfeldigvis samsvarer, betyr at den tidligste valideringen godkjennes, vanskeligere enn at alle senere blokker dekrypteres til støy. Løsningen er ikke komplisert så snart modusen er navngitt: Krypter hver 16-byte blokk med ECB og stopp kjedingen. I motoren går XlsEncryptStdPackage gjennom den utfylte bufferen i 16-byte trinn og kaller AESEncryptECB128Block på hver enkelt, som er den samme primitiven som allerede brukes for verifiseringsblokkene. Kilden har en kommentar i løkken som sier det tydelig: CBC med en null-IV matcher bare ECB for den første blokken, så resten av pakken ville dekrypteres til søppel og Excel ville avvise 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;
Feil to: RC4-omkryptringen driver ut av takt
Det eldre .xls-sporet bruker RC4 CryptoAPI-metoden, og regelen der er annerledes. [MS-OFFCRYPTO] §2.3.6 spesifiserer at chifferet omkrypteres ved hver 1024-byte blokkgrense. Strømmen er delt inn i blokker på 1024 byte, en ny RC4-nøkkel utledes for blokk nummer 0, 1, 2 osv., og innenfor hver blokk forbrukes nøkkelstrømmen kontinuerlig fra byte til byte. To invarianter må holdes sammen: Ny nøkkel ved hver grense, og forbruk av nøkkelstrømmen uten opphold inne i en blokk. RC4 er et strømchiffer, så dens nøkkelstrøm er en enkelt ordnet sekvens; den n-te byten du henter bestemmes av hvor mange byte du har hentet før den. Dekryptering er den samme XOR-operasjonen mot samme sekvens, noe som betyr at produsent og forbruker må hente nøyaktig samme byte på nøyaktig samme posisjoner.
Dette er hele utfordringen. Et strømchiffer har ingen resynkronisering. Hvis du kaster bort én byte med nøkkelstrøm, blir hver byte etter den XOR-et mot feil byte i nøkkelstrømmen, og feilen retter seg aldri; den forplanter seg til slutten av blokken og, når posisjonen først er feil, til alle blokker etter den. Feilen her gjorde akkurat det. Blokktelleren startet fra en vaktpostverdi på minus én, og skip-rutinen antok at telleren allerede matchet gjeldende blokk. Ved å starte fra denne vaktpostverdien, genererte den en ny nøkkel og kjørte en hel 1024-byte blokk med nøkkelstrøm som aldri burde vært forbrukt, og i prosessen drev den det gjenværende antallet negativt. Fra det tidspunktet var dekrypteringen en hel blokk ut av fase. Verifiseringen, som sjekkes før alt dette, ble fortsatt godkjent, slik at passordet så riktig ut mens alle dataceller kom ut som søppel.
Den korrigerte logikken lever i TXLSDecrypterRC4. Både Skip og Decrypt deler én løkke: Ny nøkkel bare når den løpende posisjonen krysser inn i en ny blokk, der blokkindeksen er posisjonen delt på REKEY_BLOCK_SIZE (1024), og forbruk deretter opptil resten av gjeldende blokk og ikke mer. MakeKey kalles med blokkindeksen, aldri med en foreldet indeks eller vaktpostindeks, og posisjonen øker med det nøyaktige antallet prosesserte byte slik at Skip og Decrypt forblir fasejustert med produsenten. Lærdommen ligger i den minste enheten: En enkelt tapt byte er ikke en liten feil i et strømchiffer, det er et totalt tap av alt 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;
Interop med en fastlåst spesifikasjon er byte-samsvar
Begge feilene reduseres til det samme underliggende prinsippet, og det er verdt å merke seg fordi det endrer hvordan du veier designvalg. Når forbrukeren av dine utdata er et eksternt, fast program du ikke kan endre på, er ikke chiffermodus og re-key-kadens implementeringsdetaljer du kan optimalisere eller forenkle. De er en del av kontrakten. Excel vil dekryptere med ECB og generere ny nøkkel på 1024-byte grenser enten disse valgene passer deg eller ikke, og din eneste jobb er å produsere byte som dekrypteres til originalen under nøyaktig denne prosedyren. En modus som er mer moderne, en IV som virker ufarlig, en teller som starter der det føles naturlig; alt dette er en feil i det øyeblikket det avviker fra hva leseren forventer. Interop mot en fastlåst spesifikasjon er ikke omtrentlig. Det er byte-nøyaktig eller så er det ødelagt.
Dette er også grunnen til at verifiseringen er en dårlig test alene. Den forteller deg at nøkkelutledningen fungerer, noe som er nødvendig, men langt fra tilstrekkelig. En test som bare åpner en kryptert fil og bekrefter at passordet godkjennes, vil rapportere suksess mens kroppen er uleselig. En reell test dekrypterer pakken og sammenligner de gjenvunne bytene med de opprinnelige inndataene, eller kjører en arbeidsbok frem og tilbake gjennom kryptering og dekryptering og leser cellene tilbake. Verifiseringen beviser passordet; bare kroppen beviser krypteringen.
Den støttede måten å lese og skrive beskyttede arbeidsbøker på
Den offentlige overflaten er liten. For å skrive en passordbeskyttet moderne arbeidsbok, fyll ut eller åpne en TXLSXWorkbook og kall SaveAsEncrypted med et filnavn og et passord; den serialiserer arbeidsboken og kjører Standard Encryption-rørledningen som den første feilen rettet, og returnerer 1 ved suksess. For å lese, kall CanReadEncrypted for å teste om en fil er en kryptert Compound File-beholder, og branch deretter: OpenEncrypted håndterer den krypterte banen og faller tilbake til Open for vanlige filer, og Open med et passord er tilgjelig direkte. Modushåndteringen og re-key-løkken beskrevet ovenfor ligger under disse kallene; du oppgir passordet og filnavnet, og motoren tar seg av spesifikasjonen 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å de beskyttede utdataene, EncryptionInfo-strømmen, verifiseringsblokkene og pakkeoppsettet er dekket i vår gjennomgang av AES-beskyttede XLSX-utdata. For det separate spørsmålet om beskyttelse på ark-nivå og hvordan beskyttelse samspiller med sideoppsett og utskrift, se artikkelen om beskyttelse, sideoppsett og utskrift. Begge bygger på krypteringsbanen beskrevet her, som leveres som en del av HotXLS-regnearkkomponenten for Delphi og C++Builder sammen med API-ene for lasting, skriving og rendring som er dekket andre steder på denne bloggen.