U schrijft een werkboek, versleutelt het met een wachtwoord, geeft het bestand aan een collega, en de collega opent het in Excel. Excel vraagt om het wachtwoord. De collega typt het in en Excel accepteert het. Tot zover lijkt de versleuteling correct. Vervolgens toont Excel een dialoogvenster dat meldt dat het bestand corrupt is en niet kan worden geopend, of het opent een werkblad met onleesbare cellen. Het wachtwoord was correct. Het bestand is toch kapot. Dit is de meest verwarrende foutmodus in Office-versleuteling, omdat het deel dat bevestigt dat het wachtwoord klopt en het deel dat uw gegevens bevat door twee verschillende bewerkingen worden beschermd. Het correct invoeren van de ene biedt geen enkele garantie voor de andere
Beide valkuilen die hier beschreven staan hebben exact deze vorm. In beide gevallen slaagt de verifier en de inhoud niet, waardoor je op zoek gaat naar een wachtwoord- of key-derivation-probleem dat er niet is. De echte fout zit stroomafwaarts, in hoe de pakketbytes getransformeerd worden. De twee valkuilen zijn onafhankelijk, één in het AES-pad en één in het RC4-pad, maar ze delen een diagnoseprobleem, dus het is de moeite waard te zien waarom een halfslachtig resultaat het lastigst te lezen is
Waarom een correct wachtwoord niets bewijst over de inhoud
Het formaat dat de moderne versleutelde XLSX gebruikt is ECMA-376 Standard Encryption, en dit slaat twee versleutelde zaken naast elkaar op. De ene is de EncryptionVerifier: een klein blok dat een willekeurige waarde en de hash van die waarde bevat, versleuteld met de sleutel die uit het wachtwoord is afgeleid. De andere is het EncryptedPackage: de volledige zip-container van het werkboek, versleuteld met dezelfde sleutel. De verificatie bestaat zodat een lezer een wachtwoord kan bevestigen voordat hij bytes van de inhoud gaat verwerken. Ontsleutel de verificatie, hash de willekeurige waarde, vergelijk deze met de opgeslagen hash, en als ze overeenkomen is het wachtwoord correct
De valkuil is dat de verificatie en het pakket door afzonderlijke aanroepen over verschillende buffers worden versleuteld. Een correct afgeleide sleutel zal de verificatie altijd correct ontsleutelen, ongeacht wat er daarna met het pakket gebeurt. Dus als uw sleutelafleiding klopt maar uw pakkettransformatie fout is, bevestigt Excel het wachtwoord via de verificatie en faalt vervolgens op de inhoud. Het symptoom luidt "correct wachtwoord, beschadigd bestand", wat het onderzoek naar het wachtwoordpad leidt, terwijl dat juist het enige deel was dat correct functioneerde. Dezelfde scheiding geldt voor het legacy RC4-geval: de verificatiehash wordt eerst gecontroleerd, en een inhoud die uit de pas loopt laat die controle ongemoeid
Valkuil één: AES in ECB, niet CBC
[MS-OFFCRYPTO] §2.3.4.15 specificeert dat Standard Encryption het pakket versleutelt met AES in Electronic Codebook (ECB) modus. Elk 16-byte blok van het opgevulde pakket wordt onafhankelijk versleuteld met dezelfde sleutel. Er is geen koppeling (chaining) tussen blokken en er is geen initialisatievector. Dit is naar moderne maatstaven een ongebruikelijke keuze (ECB wordt normaal gesproken vermeden), maar bij interoperabiliteit is het niet aan ons om de specificatie in twijfel te trekken. Excel ontsleutelt het pakket als ECB, dus een schrijver moet het als ECB versleutelen, anders stemmen ze niet overeen
De verleidelijke fout is om het pakket met AES in CBC-modus te versleutelen met een initialisatievector van alleen nullen. Dit is waarom dat bijna werkt, en waarom bijna het slechtste eindpunt is. In CBC wordt het eerste klartekstblok vóór versleuteling met de IV ge-XORd. Als de IV alleen nullen bevat verandert die XOR niets, dus het eerste blok van CBC-met-nul-IV levert exact dezelfde cijfertekst op als ECB. Vanaf het tweede blok voedt CBC het vorige cijfertekstblok in bij het volgende, dus elk blok na het eerste wijkt af van ECB
Leg dat nu naast de structuur. De pakketindeling plaatst helemaal vooraan een 8-byte little-endian lengteprefix, dus de delen van het bestand die Excel het vroegst controleert zitten in het eerste of tweede blok. Een eerste blok dat toevallig matcht betekent dat de vroegste validatie slaagt terwijl elk later blok ontsleutelt tot ruis. De remedie is niet subtiel zodra de modus een naam heeft: versleutel elk blok van 16 bytes met ECB en koppel niet. In de engine loopt XlsEncryptStdPackage de aangevulde buffer af in stappen van 16 bytes en roept per stap AESEncryptECB128Block aan, dezelfde primitieve die al voor de verifier-blokken wordt gebruikt. De broncode staat bij die lus een commentaar dat de regel kaal formuleert: CBC met een nul-IV matcht ECB alleen voor het eerste blok, dus de rest van het pakket zou ontsleutelen tot rommel en Excel zou het weigeren
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('report.xlsx');
// SaveAsEncrypted serialiseert de werkmap en voert vervolgens de
// ECMA-376 Standard Encryption-pijplijn uit: AES-128 ECB over het
// pakket volgens [MS-OFFCRYPTO] 2.3.4.15. Retourneert 1 bij succes.
if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
raise Exception.Create('Encryption failed');
finally
Book.Free;
end;
end;
Valkuil twee: de RC4 re-key raakt uit de pas
Het legacy .xls-pad maakt gebruik van het RC4 CryptoAPI-schema, en de regels daarvoor zijn wezenlijk anders. [MS-OFFCRYPTO] §2.3.6 specificeert dat het cijfer opnieuw wordt gesleuteld (re-keyed) bij elke grens van een 1024-byte blok. De stream is verdeeld in blokken van 1024 bytes, een nieuwe RC4-sleutel wordt afgeleid voor bloknummer 0, 1, 2, enzovoort, en binnen elk blok wordt de sleutelstroom continu van byte naar byte geconsumeerd. Twee invarianten moeten samen standhouden: hersleutel op elke grens, en consumeer de sleutelstroom zonder onderbrekingen binnen een blok. RC4 is een stroomcijfer, dus de sleutelstroom is een enkele geordende reeks; de n-de byte die u trekt wordt bepaald door hoeveel bytes u daarvoor hebt getrokken. Ontsleuteling is dezelfde XOR tegen dezelfde reeks, wat betekent dat schrijver en lezer exact dezelfde bytes op exact dezelfde posities moeten trekken
Dat is de hele moeilijkheid. Een stroomcijfer kent geen hersynchronisatie. Verspil je één byte keystream, dan wordt elke byte erna ge-XORd met de verkeerde keystream-byte en de fout herstelt zich nooit vanzelf; hij loopt door tot het einde van het blok en, zodra de lopende positie verkeerd zit, tot elk blok erna. Een klein boekhoudfoutje doet precies dat. Stel je een blokteller voor die start op een sentinel van min één, en een skip-routine die ervan uitgaat dat de teller al op het huidige blok staat. Vertrekkend van die sentinel her-keyt hij en verbruikt hij een vol blok keystream van 1024 bytes dat nooit had mogen worden geconsumeerd, en daarbij duwt hij de resterende teller negatief. Vanaf dat moment zit de decrypter een vol blok uit de fase. De verifier, gecontroleerd vóór alles hiervan, slaagt nog steeds, dus het wachtwoord lijkt te kloppen terwijl elke datacel als rommel uitkomt
In HotXLS zit die logica in TXLSDecrypterRC4. Zowel Skip als Decrypt delen één lus: alleen re-keyen wanneer de lopende positie een nieuw blok binnengaat, waarbij de blokindex de positie gedeeld door REKEY_BLOCK_SIZE (1024) is, en daarna hooguit tot het restant van het huidige blok consumeren. MakeKey wordt aangeroepen met de blokindex, nooit met een verouderde of sentinel-index, en de positie schuift op met exact het aantal verwerkte bytes zodat Skip en Decrypt faselageerd blijven met de producent. De les zit in de kleinste eenheid: één verspilde byte is geen kleine fout in een stroomcijfer, het is een totaal verlies van alles stroomafwaarts
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
// Een legacy .xls die met RC4 of RC4 CryptoAPI is beveiligd ontsleutelt binnen
// Open: geef het wachtwoord mee en de stream-decrypter houdt de re-key-
// cadans op de producent afgestemd.
Book.Open('legacy.xls', 'S3cret!');
// lees hier de cellen
finally
Book.Free;
end;
end;
Interoperabiliteit met een bevroren specificatie vereist exacte overeenstemming
Beide valkuilen komen op hetzelfde basisprincipe uit, en dat is het apart noemen waard omdat het bepaalt hoe je ontwerpkeuzes weegt. Als de consument van je uitvoer een vast extern programma is dat je niet kunt veranderen, dan zijn de cijfermodus en de re-key-cadans geen implementatiedetails die je mag optimaliseren of vereenvoudigen. Ze zijn deel van het draadprotocol. Excel ontsleutelt met ECB en re-keyt op grenzen van 1024 bytes of die keuzes je nu bevallen of niet, en je enige taak is bytes produceren die onder die exacte procedure ontsleutelen naar het origineel. Een modernere modus, een ogenschijnlijk onschuldige IV, een teller die begint waar het natuurlijk voelt; elk ervan is een defect op het moment dat het afwijkt van wat de lezer verwacht. Interop tegen een bevroren specificatie is niet bij benadering. Het is byte-exact of het is kapot
Dit is ook de reden waarom de verificatie op zichzelf een slechte rooktest is. Het vertelt u dat de sleutelafleiding werkt, wat noodzakelijk is maar verre van voldoende. Een test die alleen een versleuteld bestand opent en bevestigt dat het wachtwoord wordt geaccepteerd zal succes rapporteren, terwijl de inhoud onleesbaar is. Een echte test ontsleutelt het pakket en vergelijkt de herstelde bytes met de oorspronkelijke invoer, of doorloopt een werkboek via versleutelen en ontsleutelen en leest cellen terug. De verificatie bewijst het wachtwoord; alleen de inhoud bewijst de versleuteling
De ondersteunde manier om beveiligde werkboeken te lezen en te schrijven
Het publieke oppervlak is klein. Om een met wachtwoord beveiligd modern werkboek te schrijven, vul of open je een TXLSXWorkbook en roept SaveAsEncrypted aan met een bestandsnaam en een wachtwoord; die serialiseert het werkboek en doorloopt de hierboven beschreven Standard Encryption-pijplijn, en geeft 1 terug bij succes. SaveAsEncryptedAgile neemt dezelfde argumenten en schrijft het Agile-schema dat Excel 2010 en later standaard gebruiken, met SHA-512 key derivation en AES-256-pakketversleuteling. Om te lezen roep je CanReadEncrypted aan om te testen of een bestand een Compound File-container is, en dan vertak je: OpenEncrypted ontsleutelt zowel Standard- als Agile-pakketten en valt terug op Open voor gewone bestanden, en het gooit EXlsxEncryptionNotImplemented wanneer het wachtwoord verkeerd is of de container een ander schema gebruikt. Een legacy .xls-bestand is ook een Compound File, dus die gaat met het wachtwoord naar TXLSWorkbook.Open. De modusafhandeling en de re-key-lus hierboven zitten onder deze aanroepen; jij levert wachtwoord en bestandsnaam aan en de engine houdt de specificatie voor je bij
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('quarterly.xlsx');
Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
// Heropen aan de consumentzijde
Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
finally
Book.Free;
end;
end;
De vorm van de beveiligde uitvoer, de EncryptionInfo-stream, de verificatieblokken en de pakketindeling worden behandeld in onze handleiding over AES-beveiligde XLSX-uitvoer. Voor de afzonderlijke vraag naar werkbladvormige vergrendeling en hoe beveiliging samenwerkt met pagina-instellingen en afdrukken, zie het artikel over beveiliging, pagina-instellingen en afdrukken. Beide bouwen voort op het versleutelingspad dat hier wordt beschreven en dat wordt geleverd als onderdeel van de HotXLS Delphi spreadsheet component voor Delphi en C++Builder, naast de lees-, schrijf- en render-API's die elders op deze blog worden behandeld