Technischer Artikel

Warum Excel Ihr verschlüsseltes Workbook ablehnt

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 Fallen haben genau diese Form. In jedem Fall besteht die Verifizierung, der Hauptteil (Body) jedoch nicht. Das verleitet Sie, nach einem Passwort- oder Schlüsselableitungsproblem zu suchen, das gar nicht existiert. Der eigentliche Fehler sitzt weiter nachgelagert, darin, wie die Paket-Bytes transformiert werden. Die beiden Fallen sind voneinander unabhängig, eine im AES-Pfad und eine 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

Diagramm der ECMA-376-Standardverschlüsselung, wie HotXLS sie in Delphi behandelt: ein abgeleiteter Schlüssel schützt einen kleinen Verifier und das gesamte EncryptedPackage über getrennte Aufrufe
Der Verifizierer und das Paket werden durch getrennte Aufrufe über getrennte Puffer verschlüsselt, daher beweist ein korrektes Passwort nichts über den Körper

Falle 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 verlockende Fehler ist, das Paket mit AES im CBC-Modus und einem Null-Initialisierungsvektor zu verschlüsseln. 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. Bestand der IV ausschließlich aus Nullen, ändert dieses XOR 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 Kur ist nicht subtil, sobald der Modus feststeht: Verschlüsseln Sie jeden 16-Byte-Block mit ECB und verketten Sie nicht. 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üsseln würde und Excel ihn ablehnen würde

HotXLS-Diagramm: AES ECB gegenüber CBC mit Null-IV über die 16-Byte-Blöcke eines verschlüsselten XLSX-Pakets, wobei nur der erste Block übereinstimmt und jeder spätere Block abweicht
CBC mit Null-IV reproduziert ECB nur für den ersten Block, der das Längenpräfix trägt, das Excel am frühesten validiert, während jeder spätere Block zu Rauschen entschlüsselt
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted serialisiert die Arbeitsmappe und führt dann die
    // ECMA-376-Standardverschlüsselungs-Pipeline aus: AES-128-ECB über das
    // Paket gemäß [MS-OFFCRYPTO] 2.3.4.15. Gibt bei Erfolg 1 zurück.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Falle zwei: Der RC4-Re-Key driftet 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 einziges 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. Genau das tut ein kleiner Buchhaltungsfehler. Stellen Sie sich einen Blockzähler vor, der bei einem Wächterwert von minus eins startet, und eine Überspringungsroutine, die annimmt, der Zähler passe bereits zum aktuellen Block. Ausgehend von diesem Wächterwert macht sie einen Re-Key und lässt einen vollen 1024-Byte-Block Keystream laufen, der nie hätte verbraucht werden dürfen, und treibt dabei den Restzähler ins Negative. Ab diesem Punkt ist der Entschlüsseler um einen vollen Block phasenverschoben. Der Verifizierer, der vor all dem geprüft wird, besteht trotzdem, sodass das Passwort richtig aussieht, während jede Datenzelle als Müll herauskommt

In HotXLS liegt diese Logik in TXLSDecrypterRC4. Skip und Decrypt teilen sich eine Schleife: Re-Key nur, wenn die laufende Position in einen neuen Block eintritt, wobei der Blockindex die Position geteilt durch REKEY_BLOCK_SIZE (1024) ist, und danach höchstens bis zum Rest des aktuellen Blocks verbrauchen. MakeKey wird mit dem Blockindex aufgerufen, niemals mit einem veralteten oder Wächterindex, und die Position rückt um genau die Anzahl der verarbeiteten Bytes vor, sodass Skip und Decrypt phasenbündig mit dem Ersteller bleiben. Die Lektion liegt in der kleinsten Einheit: Ein einziges verschwendetes Byte ist bei einer Stromverschlüsselung kein kleiner Fehler, sondern der Totalverlust von allem, was danach kommt

Diagramm des RC4-Rekey-Drifts in Legacy-Excel-Verschlüsselung, gelesen aus Delphi: der Produzent re-keyt alle 1024 Bytes, während ein Sentinel-Start-Verbraucher einen Keystream-Block verbrennt und außer Phase bleibt
Ein Stromchiffrierer hat keine Resynchronisation, daher lässt ein einziger verbrannter 1024-Byte-Schlüsselstromblock jede entschlüsselte Zelle als Müll zurück, während der Verifizierer weiterhin besteht
var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create;
  try
    // Ein legacy .xls mit RC4 oder RC4 CryptoAPI entschlüsselt innerhalb
    // von Open: Passwort übergeben, der Strom-Entschlüsseler hält den Re-Key-
    // Takt phasenbündig zum Ersteller.
    Book.Open('legacy.xls', 'S3cret!');
    // hier die Zellen lesen
  finally
    Book.Free;
  end;
end;

Interoperabilität mit einer eingefrorenen Spezifikation bedeutet Übereinstimmung bis zum Byte

Beide Fallen lassen sich auf dasselbe Grundprinzip reduzieren, und es lohnt sich, es eigenständig zu formulieren, weil es verändert, wie Sie Designentscheidungen gewichten. Wenn der Empfänger Ihrer Ausgabe ein festes externes Programm ist, das Sie nicht ändern können, sind Verschlüsselungsmodus und Re-Key-Takt keine Implementierungsdetails, die Sie optimieren oder vereinfachen dürften. Sie sind Teil des Übertragungsvertrags. Excel entschlüsselt mit ECB und macht alle 1024 Byte einen Re-Key, ob Ihnen diese Entscheidungen gefallen oder nicht, und Ihre einzige Aufgabe ist es, Bytes zu erzeugen, die unter genau diesem Verfahren zum Original entschlüsseln. Ein modernerer Modus, ein harmlos wirkender IV, ein Zähler, der dort beginnt, wo es sich natürlich anfühlt; all das ist in dem Moment ein Defekt, in dem es von dem abweicht, was der Reader erwartet. Interop gegen eine eingefrorene Spezifikation ist nicht näherungsweise. Sie ist byte-genau oder sie ist kaputt

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 Dateinamen und Passwort auf; es serialisiert das Workbook, durchläuft die oben beschriebene Standard-Encryption-Pipeline und gibt bei Erfolg 1 zurück. SaveAsEncryptedAgile nimmt dieselben Argumente und schreibt das Agile-Schema, das Excel 2010 und neuer standardmäßig verwenden, mit SHA-512-Schlüsselableitung und AES-256-Paketverschlüsselung. Zum Lesen rufen Sie CanReadEncrypted auf, um zu testen, ob eine Datei ein Compound-File-Container ist, und verzweigen dann: OpenEncrypted entschlüsselt sowohl Standard- als auch Agile-Pakete und fällt für einfache Dateien auf Open zurück, und es wirft EXlsxEncryptionNotImplemented, wenn das Passwort falsch ist oder der Container ein anderes Schema verwendet. Eine legacy .xls-Datei ist ebenfalls ein Compound File, sie geht also stattdessen an TXLSWorkbook.Open mit dem Passwort. Die Modusbehandlung und die oben beschriebene Re-Key-Schleife liegen unterhalb dieser Aufrufe; Sie übergeben Passwort und Dateinamen, und die Engine erfüllt die Spezifikation für Sie

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // auf der Verbraucherseite erneut öffnen
    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-Delphi-Tabellenkalkulationskomponente für Delphi und C++Builder zusammen mit den an anderer Stelle auf diesem Blog behandelten Lese-, Schreib- und Rendering-APIs ausgeliefert wird