Technical Article

Hvorfor Excel afviser din krypterede projektmappe: ECB og RC4

Du skriver en projektmappe, krypterer den med en adgangskode, giver 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 er beskadiget og ikke kan åbnes, eller den åbnes med et ark af meningsløse celler. Adgangskoden var rigtig. Filen er ødelagt alligevel. Dette er den mest forvirrende fejltilstand 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, og at få den ene korrekt garanterer intet om den anden.

Begge fejl beskrevet her havde præcis denne form. I hvert tilfælde lykkedes verifikatoren, og kroppen gjorde ikke, hvilket sender dig på jagt efter en adgangskode- eller nøgleudledningsfejl, der ikke er der. Den reelle fejl lå længere nede i strømmen, i hvordan pakkens bytes blev transformeret. De to fejl er uafhængige, den ene i AES-stien og den anden i RC4-stien, men de deler et diagnosticeringsproblem, så det er værd at se, hvorfor et halv-korrekt resultat er det sværeste at tolke.

Hvorfor en bestået adgangskode intet beviser om kroppen

Det format, den moderne krypterede XLSX bruger, er ECMA-376 Standard Encryption, og det gemmer to krypterede ting side om side. Den ene er EncryptionVerifier: en lille blok, der indeholder en tilfældig værdi og hash-værdien af denne værdi, krypteret med den nøgle, der er udledt af adgangskoden. Den anden er EncryptedPackage: hele zip-containeren for projektmappen, krypteret med den samme nøgle. Verifikatoren findes, så en læser kan bekræfte en adgangskode, før den bruger kræfter på megabytes af krop. Decrypt the verifier, hash the random value, compare it to the stored hash, and if they match the password is correct.

Fælden er, at verifikatoren og pakken er krypteret ved separate kald over separate buffere. En nøgle, der er udledt korrekt, vil dekryptere verifikatoren korrekt, uanset hvad der sker med pakken bagefter. Så hvis din nøgleudledning er rigtig, men din pakketransformation er forkert, bekræfter Excel adgangskoden fra verifikatoren og fejler derefter på kroppen. Symptomet lyder som "rigtig adgangskode, ødelagt fil", hvilket peger efterforskningen mod adgangskodestien, som var den ene del, der aldrig var i stykker. Den samme adskillelse styrer det ældre RC4-tilfælde: verifikatorens hash kontrolleres først, og en krop, der skrider ud af synkronisering, efterlader stadig den kontrol intakt.

Fejl ét: AES i ECB, ikke CBC

[MS-OFFCRYPTO] §2.3.4.15 specifies that Standard Encryption encrypts the package with AES in Electronic Codebook mode. Every 16-byte block of the padded package is encrypted independently with the same key. There is no chaining between blocks and there is no initialization vector. This is an unusual choice by modern standards, where ECB is normally avoided, but interop is not a place to second-guess the specification. Excel dekrypterer pakken som ECB, så en producent skal kryptere den som ECB, ellers bliver de to ikke enige.

Fejlen var, at pakken blev krypteret med AES i CBC-tilstand ved hjælp af en initialiseringsvektor med lutter nuller. Here is why that almost works, and why almost is the worst place to land. In CBC, the first plaintext block is XORed with the IV before encryption. When the IV is all zeros, that XOR changes nothing, so the first block of CBC-with-zero-IV produces exactly the same ciphertext as ECB. From the second block onward CBC feeds the previous ciphertext block into the next, so every block after the first diverges from ECB.

Læg nu det oven på strukturen. Pakkelayoutet placerer et 8-byte little-endian længdepræfiks i starten, så de dele af filen, Excel kontrollerer tidligst, sidder i de første en eller to blokke. En første blok, der tilfældigvis matcher, betyder, at den tidligste validering lykkes, mens hver senere blok dekrypteres to støj. Løsningen er ikke subtil, når tilstanden først er navngivet: krypter hver 16-byte blok med ECB og stop med at kæde. I motoren går XlsEncryptStdPackage gennem den polstrede buffer in 16-byte steps and calls AESEncryptECB128Block on each one, which is the same primitive already used for the verifier blocks. Kilden har en kommentar ved løkken, der tydeligt angiver reglen: CBC med en nul-IV matcher kun ECB for den første blok, så resten af pakken ville dekryptere 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 re-key driver ud af takt

Den ældre .xls-sti bruger RC4 CryptoAPI-skemaet, og dens regel er anderledes. [MS-OFFCRYPTO] §2.3.6 specifies that the cipher is re-keyed at every 1024-byte block boundary. The stream is divided into blocks of 1024 bytes, a fresh RC4 key is derived for block number 0, 1, 2, and so on, and within each block the keystream is consumed continuously from byte to byte. Two invariants have to hold together: re-key on each boundary, and consume the keystream without gaps inside a block. RC4 is a stream cipher, so its keystream is a single ordered sequence; the n-th byte you draw is determined by how many bytes you have drawn before it. Decryption is the same XOR against the same sequence, which means producer and consumer must draw exactly the same bytes at exactly the same positions.

Det er hele vanskeligheden. A stream cipher has no resynchronization. If you waste one byte of keystream, every byte after it is XORed against the wrong keystream byte, and the error never corrects itself; it cascades to the end of the block and, once the running position is wrong, to every block after it. Fejlen her gjorde netop det. Bloktælleren startede fra en sentinel-værdi på minus et, og skip-rutinen antog, at tælleren allerede matchede den aktuelle blok. Startende fra denne sentinel-værdi gen-nøglede og kørte den en hel 1024-byte blok af nøglestrøm, som aldrig skulle have været forbrugt, og i processen kørte den det resterende antal negativt. Fra det tidspunkt var dekrypteringen en hel blok ude af fase. Verifikatoren, der blev kontrolleret før alt dette, bestod stadig, så adgangskoden så rigtig ud, mens hver datacelle kom ud som affald.

Den korrigerede logik lever i TXLSDecrypterRC4. Både Skip og Decrypt deler én løkke: gen-nøgle kun, når den kørende 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, under no circumstances with a stale or sentinel index, and the position advances by the exact number of bytes processed so that Skip and Decrypt stay phase-aligned with the producer. Lektionen ligger i den mindste enhed: en enkelt spildt byte er ikke en lille fejl i en strøm-cipher, det er et totalt tab af alt downstream.

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 specifikation er at matche til byten

Begge fejl reduceres til det samme grundprincip, og det er værd at slå fast for sig selv, fordi det ændrer, hvordan du afvejer designvalg. Når forbrugeren af dit output er et fast eksternt program, du ikke kan ændre, er cipher-tilstanden og gen-nøglingskadencen ikke implementeringsdetaljer, du kan optimere eller forenkle. De er en del af kontraktens indhold. Excel vil dekryptere med ECB og gen-nøgle på 1024-byte grænser, uanset om disse valg behager dig eller ej, og din eneste opgave er at producere bytes, der dekrypterer til det originale under nøjagtig den procedure. A mode that is more modern, an IV that seems harmless, a counter that starts where it feels natural; any of these is a defect the instant it diverges from what the reader expects. Interop mod en fastlåst specifikation er ikke omtrentlig. Det er byte-præcist, eller det er i stykker.

Dette er også grunden til, at verifikatoren er en dårlig røgtest i sig selv. 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 bestås, vil rapportere succes, mens kroppen er ulæselig. A real test decrypts the package and compares the recovered bytes to the original input, or round-trips a workbook through encrypt and decrypt and reads cells back. Verifikatoren beviser adgangskoden; kun kroppen beviser krypteringen.

Den understøttede måde at læse og skrive beskyttede projektmapper på

Den offentlige overflade er lille. For at skrive en adgangskodebeskyttet moderne projektmappe, udfyld eller åbn en TXLSXWorkbook og kald SaveAsEncrypted med et filnavn og en adgangskode; den serialserer projektmappen og kører Standard Encryption-pipelinen, som den første rettelse korrigerede, og returnerer 1 ved succes. For at læse, kald CanReadEncrypted for at teste, om en fil er en krypteret Compound File-container, og forgrening derefter: OpenEncrypted håndterer den krypterede sti og falder tilbage til Open for almindelige filer, og Open med en adgangskode er tilgængelig direkte. Tilstandshåndteringen og gen-nøglingsløkken beskrevne ovenfor sidder 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, verifikationsblokkene og pakkelayoutet er dækket i vores gennemgang af AES-beskyttet XLSX-output. For det separate spørgsmål om arkbeskyttelse, 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-komponenten til Delphi og C++Builder sammen med API'erne til læsning, skrivning og rendering, der er dækket andre steder på denne blog.