Sie schreiben ein Workbook, verschlüsseln es mit einem Passwort, übergeben die Datei einem Kollegen, und dieser öffnet sie in Excel. Excel fragt nach dem Passwort. Der Kollege gibt es ein, und Excel akzeptiert es. Bis hierhin sieht die Verschlüsselung korrekt aus. Dann blendet Excel einen Dialog ein, der besagt, dass die Datei beschädigt ist und nicht geöffnet werden kann, oder sie öffnet sich mit einer Tabelle aus bedeutungslosen Zellen. Das Passwort war richtig. Die Datei ist trotzdem defekt. Dies ist der verwirrendste Fehlermodus in der Office-Verschlüsselung, da der Teil, der Ihnen mitteilt, dass das Passwort richtig ist, und der Teil, der Ihre Daten enthält, durch zwei verschiedene Operationen geschützt sind, und die Korrektheit des einen keineswegs die des anderen garantiert
Beide hier beschriebenen Fehler hatten genau diese Form. In jedem Fall war die Verifizierung erfolgreich, der Hauptteil (Body) jedoch nicht. Dies verleitet Sie dazu, nach einem Passwort- oder Schlüsselableitungsfehler zu suchen, der gar nicht existiert. Der tatsächliche Fehler lag weiter nachgelagert darin, wie die Paket-Bytes transformiert wurden. Die beiden Fehler sind voneinander unabhängig, einer im AES-Pfad und einer im RC4-Pfad, aber sie teilen ein Diagnoseproblem. Daher lohnt es sich zu sehen, warum ein halb korrektes Ergebnis am schwierigsten zu interpretieren ist
Warum ein erfolgreiches Passwort nichts über den Hauptteil beweist
Das Format, das das moderne verschlüsselte XLSX verwendet, ist die Standardverschlüsselung nach ECMA-376, und es speichert zwei verschlüsselte Dinge nebeneinander. Das eine ist der EncryptionVerifier: ein kleiner block, der einen Zufallswert und den Hash dieses Wertes enthält, verschlüsselt mit dem aus dem Passwort abgeleiteten Schlüssel. Das andere ist das EncryptedPackage: der gesamte Zip-Container des Workbooks, verschlüsselt mit demselben Schlüssel. Der Verifizierer existiert, damit ein Reader ein Passwort bestätigen kann, bevor er Aufwand für Megabytes an Hauptteil betreibt. Entschlüsseln Sie den Verifizierer, berechnen Sie den Hash des Zufallswerts, vergleichen Sie ihn mit dem gespeicherten Hash, und wenn sie übereinstimmen, ist das Passwort korrekt
Die Falle besteht darin, dass der Verifizierer und das Paket durch separate Aufrufe über separate Puffer verschlüsselt werden. Ein korrekt abgeleiteter Schlüssel entschlüsselt den Verifizierer korrekt, unabhängig davon, was danach mit dem Paket geschieht. Wenn also Ihre Schlüsselableitung richtig, aber Ihre Pakettransformation falsch ist, Excel bestätigt das Passwort anhand des Verifizierers und scheitert dann am Hauptteil. Das Symptom lautet „richtiges Passwort, defekte Datei", was die Untersuchung auf den Passwortpfad lenkt - also den einzigen Teil, der nie fehlerhaft war. Dieselbe Trennung gilt für den veralteten RC4-Fall: Der Verifizierer-Hash wird zuerst geprüft, und ein Hauptteil, der aus dem Takt gerät, lässt diese Prüfung dennoch unberührt
Fehler eins: AES in ECB, nicht CBC
[MS-OFFCRYPTO] §2.3.4.15 legt fest, dass die Standardverschlüsselung das Paket mit AES im Electronic-Codebook-Modus (ECB) verschlüsselt. Jeder 16-Byte-Block des gefüllten Pakets wird unabhängig mit demselben Schlüssel verschlüsselt. Es gibt keine Verkettung zwischen den Blöcken und keinen Initialisierungsvektor. Dies ist nach modernen Standards eine ungewöhnliche Wahl, da ECB normalerweise vermieden wird, aber Interoperabilität ist kein Ort, um die Spezifikation infrage zu stellen. Excel entschlüsselt das Paket als ECB, daher muss ein Ersteller es als ECB verschlüsseln, da die beiden sonst nicht übereinstimmen
Der Fehler bestand darin, dass das Paket mit AES im CBC-Modus unter Verwendung eines Null-Initialisierungsvektors verschlüsselt wurde. Hier ist der Grund, warum das fast funktioniert, und warum „fast" der schlechteste Ort ist, an dem man landen kann. Im CBC-Modus wird der erste Klartextblock vor der Verschlüsselung mit dem IV per XOR verknüpft. Wenn der IV ausschließlich aus Nullen besteht, dieses XOR ändert nichts, sodass der erste Block von CBC-mit-Null-IV exakt denselben Chiffretext wie ECB erzeugt. Ab dem zweiten Block speist CBC den vorherigen Chiffretextblock in den nächsten ein, sodass jeder Block nach dem ersten von ECB abweicht
Übertragen Sie das nun auf die Struktur. Das Paket-Layout platziert ein 8 Byte langes Little-Endian-Längenpräfix ganz am Anfang, sodass die Teile der Datei, die Excel am frühesten prüft, im ersten oder zweiten Block liegen. Ein erster Block, der zufällig übereinstimmt, bedeutet, dass die früheste Validierung erfolgreich ist, während jeder spätere Block zu Rauschen entschlüsselt wird. Die Behebung ist nicht subtil, sobald der Modus feststeht: Verschlüsseln Sie jeden 16-Byte-Block mit ECB und beenden Sie die Verkettung. In der Engine durchläuft XlsEncryptStdPackage den gefüllten Puffer in 16-Byte-Schritten und ruft für jeden einzelnen AESEncryptECB128Block auf - dasselbe Primitiv, das bereits für die Verifizierer-Blöcke verwendet wird. Der Quellcode enthält an der Schleife einen Kommentar, der die Regel klar formuliert: CBC mit einem Null-IV stimmt nur für den ersten Block mit ECB überein, sodass der Rest des Pakets zu Müll entschlüsselt würde und Excel ihn ablehnen würde
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;
Fehler zwei: Der RC4-Re-Key-Drift aus dem Takt
Der veraltete .xls-Pfad verwendet das RC4-CryptoAPI-Schema, und seine Regel ist anderer Natur. [MS-OFFCRYPTO] §2.3.6 legt fest, dass die Verschlüsselung an jeder 1024-Byte-Blockgrenze neu initialisiert (re-keyed) wird. Der Stream ist in Blöcke von 1024 Byte unterteilt. Ein neuer RC4-Schlüssel wird für die Blocknummern 0, 1, 2 usw. abgeleitet, und innerhalb jedes Blocks wird der Keystream kontinuierlich von Byte zu Byte verbraucht. Zwei Invarianten müssen zusammenpassen: Neuinitialisierung an jeder Grenze und Verbrauch des Keystreams ohne Lücken innerhalb eines Blocks. RC4 is eine Stromverschlüsselung, sodass sein Keystream eine einzelne geordnete Sequenz ist; das n-te Byte, das Sie ziehen, wird dadurch bestimmt, wie viele Bytes Sie zuvor gezogen haben. Die Entschlüsselung ist dasselbe XOR gegen dieselbe Sequenz, was bedeutet, dass Ersteller und Empfänger exakt dieselben Bytes an exakt denselben Positionen ziehen müssen
Das ist die ganze Schwierigkeit. Eine Stromverschlüsselung hat keine Resynchronisation. Wenn Sie ein einzelnes Byte des Keystreams verschwenden, wird jedes Byte danach gegen das falsche Keystream-Byte per XOR verknüpft, und der Fehler korrigiert sich nie von selbst. Er kaskadiert bis zum Ende des Blocks und - sobald die laufende Position falsch ist - auf jeden Block danach. Der Fehler hier tat genau das. Der Blockzähler startete bei einem Wächterwert von minus eins, und die Überspringungsroutine ging davon aus, dass der Zähler bereits mit dem aktuellen Block übereinstimmte. Ausgehend von diesem Wächter führte sie eine Neuinitialisierung durch und ließ einen vollen 1024-Byte-Block des Keystreams laufen, der niemals hätte verbraucht werden dürfen, wodurch der verbleibende Zähler negativ wurde. Ab diesem Punkt war der Entschlüsseler um einen vollen Block phasenverschoben. Der Verifizierer, der vor all dem geprüft wurde, war dennoch erfolgreich, sodass das Passwort richtig aussah, während jede Datenzelle als Müll herauskam
Die korrigierte Logik befindet sich in TXLSDecrypterRC4. Sowohl Skip und Decrypt teilen sich eine Schleife: Führe die Neuinitialisierung nur durch, wenn die laufende Position in einen neuen Block eintritt, wobei der Blockindex die Position geteilt durch REKEY_BLOCK_SIZE (1024) ist, und verbrauche dann bis zum Rest des aktuellen Blocks und nicht mehr. MakeKey wird mit dem Blockindex aufgerufen, niemals mit einem veralteten oder Wächterindex, und die Position erhöht sich um die genaue Anzahl der verarbeiteten Bytes, sodass Skip und Decrypt phasenbündig mit dem Ersteller bleiben. Die Lektion liegt in der kleinsten Einheit: Ein einziges verschwendetes Byte ist kein kleiner Fehler in einer Stromverschlüsselung, sondern ein Totalverlust von allem, was danach kommt
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;
Interoperabilität mit einer eingefrorenen Spezifikation bedeutet Übereinstimmung bis zum Byte
Beide Fehler lassen sich auf dasselbe Grundprinzip reduzieren, und es ist wert, eigenständig formuliert zu werden, da es die Gewichtung von Designentscheidungen verändert. Wenn der Empfänger Ihrer Ausgabe ein festes externes Programm ist, das Sie nicht ändern können, sind der Verschlüsselungsmodus und die Neuinitialisierungsfrequenz keine Implementierungsdetails, die Sie optimieren oder vereinfachen können. Sie sind Teil des festen Vertrags. Excel entschlüsselt mit ECB und führt alle 1024 Byte eine Neuinitialisierung durch, ob Ihnen diese Entscheidungen gefallen oder nicht, und Ihre einzige Aufgabe ist es, Bytes zu erzeugen, die unter genau diesem Verfahren in das Original entschlüsselt werden. Ein modernerer Modus, ein harmlos wirkender IV, ein Zähler, der dort beginnt, wo es sich natürlich anfühlt - all dies ist im selben Moment ein Fehler, in dem es von dem abweicht, was der Reader erwartet. Interoperabilität gegen eine eingefrorene Spezifikation ist nicht näherungsweise. Sie ist byte-genau oder fehlerhaft
Dies ist auch der Grund, warum der Verifizierer allein ein schlechter Smoke-Test ist. Er sagt Ihnen, dass die Schlüsselableitung funktioniert, was zwar notwendig, aber bei Weitem nicht ausreichend ist. Ein Test, der nur eine verschlüsselte Datei öffnet und bestätigt, dass das Passwort funktioniert, meldet Erfolg, während der Hauptteil unlesbar ist. Ein echer Test entschlüsselt das Paket und vergleicht die wiederhergestellten Bytes mit der ursprünglichen Eingabe oder führt ein Workbook durch Verschlüsselung und Entschlüsselung im Round-Trip und liest die Zellen zurück. Der Verifizierer beweist das Passwort; nur der Hauptteil beweist die Verschlüsselung
Der unterstützte Weg zum Lesen und Schreiben geschätzter Workbooks
Die öffentliche Schnittstelle ist klein. Um ein passwortgeschütztes modernes Workbook zu schreiben, befüllen oder öffnen Sie ein TXLSXWorkbook und rufen Sie SaveAsEncrypted mit einem Dateinamen und einem Passwort auf. Dies serialisiert das Workbook und führt die Standard-Verschlüsselungspipeline aus, die die erste Behebung korrigiert hat, und gibt bei Erfolg 1 zurück. Zum Lesen rufen Sie CanReadEncrypted auf, um zu testen, ob es sich bei einer Datei um einen verschlüsselten Compound-File-Container handelt, und verzweigen dann: OpenEncrypted verarbeitet den verschlüsselten Pfad und fällt für einfache Dateien auf Open zurück, und Open mit einem Passwort ist direkt verfügbar. Die Modushandhabung und die oben beschriebene Re-Key-Schleife liegen unter diesen Aufrufen; Sie übergeben das Passwort und den Dateinamen, und die Engine erfüllt die Spezifikation in Ihrem Namen
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;
Die Struktur der geschützten Ausgabe, der EncryptionInfo-Stream, die Verifizierer-Blöcke und das Paket-Layout werden in our walkthrough of AES-protected XLSX output behandelt. Für die separate Frage der Sperrung auf Blattebene und wie sich der Schutz auf das Seiten-Setup und das Drucken auswirkt, lesen Sie den Artikel über Schutz, Seiten-Setup und Drucken. Beide bauen auf dem hier beschriebenen Verschlüsselungspfad auf, der als Teil der HotXLS-Tabellenkalkulationskomponente für Delphi und C++Builder zusammen mit den an anderer Stelle auf diesem Blog behandelten Lese-, Schreib- und Rendering-APIs ausgeliefert wird