Articolo tecnico

Perché Excel rifiuta la tua cartella di lavoro

Quando si scrive una cartella di lavoro, la si protegge con password, la si invia a un collega ed egli la apre in Excel, il programma richiede la password. Il collega la inserisce ed Excel la accetta. Fin qui il processo sembra corretto. Successivamente, Excel mostra una finestra di dialogo in cui segnala che il file è corrotto e non può essere aperto, oppure si apre mostrando celle con dati privi di significato. La password era corretta, ma il file risulta danneggiato. Questa è una delle anomalie più complesse nella crittografia di Office, poiché la sezione che convalida la password e quella che memorizza i dati sono protette da operazioni diverse, e completare correttamente la prima non garantisce il successo della seconda

Entrambe le trappole descritte qui hanno esattamente questa forma. In ciascun caso il verifier passa e il corpo del file no, il che vi manda a caccia di un problema di password o di derivazione della chiave che non c'è. Il difetto vero sta a valle, in come vengono trasformati i byte del pacchetto. Le due trappole sono indipendenti, una nel percorso AES e una nel percorso RC4, ma condividono il problema di diagnosi, quindi vale la pena vedere perché un risultato mezzo corretto è il più difficile da leggere

Perché la convalida della password non garantisce la correttezza del file

Il formato utilizzato dai file XLSX protetti moderni è lo standard ECMA-376 Standard Encryption, che memorizza due elementi crittografati affiancati. Il primo è l'EncryptionVerifier: una piccola area contenente un valore casuale e il rispettivo hash, crittografati con la chiave derivata dalla password. Il secondo è l'EncryptedPackage: l'intero contenitore zip della cartella di lavoro, crittografato con la stessa chiave. Il verifier esiste per consentire al lettore di verificare una password prima di decrittografare l'intero file. Decrittografando il verifier ed eseguendo l'hash del valore casuale è possibile stabilire se la password sia corretta confrontandola con l'hash memorizzato

La particolarità risiede nel fatto che il verifier e il pacchetto vengono crittografati tramite chiamate separate su buffer differenti. Una chiave derivata correttamente decrittograferà il verifier senza considerare lo stato del pacchetto successivo. Pertanto, se la derivazione della chiave è corretta ma la trasformazione del pacchetto presenta errori, Excel accetterà la password ma segnalerà errori per il corpo del file. Il sintomo si manifesta come "password corretta, file corrotto", indirizzando l'analisi sul percorso della password, l'unico elemento che non presentava problemi. La stessa separazione governa il caso RC4 legacy: l'hash del verifier viene verificato per primo, e se il corpo del file presenta disallineamenti il controllo iniziale viene comunque superato

Diagramma della Standard Encryption ECMA-376 come gestita da HotXLS in Delphi: una chiave derivata protegge un piccolo verifier e l'intero EncryptedPackage attraverso chiamate separate
Il verifier e il pacchetto sono cifrati da chiamate separate su buffer separati, così una password corretta non prova nulla sul corpo

Trappola uno: AES in ECB, non CBC

La specifica [MS-OFFCRYPTO] §2.3.4.15 indica che lo Standard Encryption protegge il pacchetto tramite AES in modalità Electronic Codebook (ECB). Ogni blocco da 16 byte del pacchetto viene crittografato in modo indipendente con la stessa chiave. Non vi è concatenamento tra i blocchi né vettori di inizializzazione. Si tratta di una scelta insolita rispetto agli standard moderni, in cui la modalità ECB viene solitamente evitata, ma la compatibilità richiede il rispetto preciso della specifica. Excel decrittografa il pacchetto in modalità ECB, quindi il produttore deve applicare tale algoritmo per garantire l'allineamento

Lo sbaglio allettante è cifrare il pacchetto con AES in modalità CBC usando un vettore di inizializzazione tutto a zero. Ecco perché quasi funziona, e perché il quasi è il punto peggiore dove atterrare. In CBC il primo blocco di plaintext viene messo in XOR con l'IV prima della cifratura. Con un IV tutto a zero quella XOR non cambia niente, quindi il primo blocco di CBC-con-IV-a-zero produce esattamente lo stesso ciphertext di ECB. Dal secondo blocco in poi CBC alimenta il ciphertext precedente nel blocco successivo, quindi ogni blocco dopo il primo diverge da ECB

Ora sovrapponete questo alla struttura. Il layout del pacchetto mette un prefisso di lunghezza little-endian a 8 byte proprio all'inizio, quindi le parti del file che Excel controlla prima stanno nel primo blocco o nei primi due. Un primo blocco che combacia per caso fa passare la validazione più precoce mentre ogni blocco successivo si decifra in rumore. La cura non è sottile una volta nominata la modalità: cifrate ogni blocco da 16 byte con ECB e non concatenate. Nel motore, XlsEncryptStdPackage percorre il buffer con padding a passi di 16 byte e chiama AESEncryptECB128Block su ciascuno, la stessa primitiva già usata per i blocchi del verifier. Il sorgente porta un commento al ciclo che dichiara la regola alla schietta: CBC con IV a zero combacia con ECB solo per il primo blocco, quindi il resto del pacchetto si deciferebbe in spazzatura e Excel lo rifiuterebbe

Diagramma HotXLS di AES ECB contro CBC con IV zero sui blocchi da 16 byte di un pacchetto XLSX cifrato, dove solo il primo blocco coincide e ogni blocco successivo diverge
CBC con IV nullo riproduce ECB solo per il primo blocco, che custodisce il prefisso di lunghezza che Excel valida per primo, mentre ogni blocco successivo si decifra in rumore
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted serializza la cartella di lavoro, poi esegue la
    // pipeline di cifratura standard ECMA-376: AES-128 ECB sul
    // pacchetto secondo [MS-OFFCRYPTO] 2.3.4.15. Restituisce 1 in caso di successo.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Trappola due: il re-key RC4 che esce di fase

Il percorso legacy .xls adotta lo schema RC4 CryptoAPI, e la regola è differente. La specifica [MS-OFFCRYPTO] §2.3.6 indica che la cifratura viene rieseguita (re-key) a ogni confine di blocco da 1024 byte. Lo stream viene suddiviso in blocchi da 1024 byte, viene derivata una nuova chiave RC4 per ciascun blocco e all'interno di ogni blocco lo stream di chiavi (keystream) viene consumato in modo continuo. È necessario garantire che il re-key avvenga a ogni confine e che lo stream di chiavi sia consumato senza interruzioni all'interno del blocco. Essendo l'RC4 un cifrario a flusso, lo stream di chiavi è una sequenza ordinata; l'ennesimo byte estratto è definito dalle operazioni precedenti. La decrittografia esegue lo XOR con la medesima sequenza, richiedendo che produttore e consumatore leggano gli stessi byte nelle medesime posizioni

Questa è tutta la difficoltà. Un cifrario a flusso non ha risincronizzazione. Se buttate un byte di keystream, ogni byte successivo viene messo in XOR contro il byte di keystream sbagliato, e l'errore non si corregge mai da solo; va a cascata fino alla fine del blocco e, una volta che la posizione corrente è sbagliata, a ogni blocco dopo di esso. Una piccola svista di contabilità fa esattamente questo. Immaginate un contatore di blocchi che parte da un valore sentinella di meno uno, e una routine di salto che assume che il contatore combaci già con il blocco corrente. Partendo da quella sentinella, esegue il re-key e consuma un intero blocco da 1024 byte di keystream che non sarebbe mai dovuto essere consumato, e nel frattempo porta il conteggio residuo sotto zero. Da quel punto il decrypter è fuori fase di un intero blocco. Il verifier, controllato prima di tutto questo, passa ancora, quindi la password sembra giusta mentre ogni cella di dati esce come spazzatura

In HotXLS la logica vive in TXLSDecrypterRC4. Skip e Decrypt condividono lo stesso ciclo: re-key solo quando la posizione corrente entra in un nuovo blocco, dove l'indice di blocco è la posizione divisa per REKEY_BLOCK_SIZE (1024), poi consumo fino al resto del blocco corrente e non oltre. MakeKey viene chiamato con l'indice di blocco, mai con un indice obsoleto o sentinella, e la posizione avanza del numero esatto di byte elaborati, così Skip e Decrypt restano in fase con il produttore. La lezione sta nell'unità più piccola: un solo byte buttato non è un piccolo errore in un cifrario a flusso, è la perdita totale di tutto ciò che viene dopo

Diagramma della deriva del re-key RC4 nella cifratura Excel legacy letta da Delphi: il produttore rigenera la chiave ogni 1024 byte mentre un consumatore con partenza sentinella brucia un blocco di keystream e resta fuori fase
Un cifrario a flusso non ha risincronizzazione, così un solo blocco di keystream da 1024 byte bruciato lascia ogni cella decifrata come spazzatura mentre il verifier continua a passare
var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create;
  try
    // Un .xls legacy protetto con RC4 o RC4 CryptoAPI si decifra dentro
    // Open: passate la password e il decrypter a stream tiene il re-key
    // in fase con il produttore.
    Book.Open('legacy.xls', 'S3cret!');
    // leggi le celle qui
  finally
    Book.Free;
  end;
end;

Garantire la conformità byte per byte

Entrambe le trappole si riconducono allo stesso principio di fondo, e vale la pena enunciarlo da solo perché cambia il peso delle scelte di progetto. Quando il consumatore del vostro output è un programma esterno fisso che non potete cambiare, la modalità di cifratura e il ritmo del re-key non sono dettagli implementativi da ottimizzare o semplificare a piacere. Fanno parte del contratto sul filo. Excel decifrerà con ECB e rifarà il re-key sui confini di 1024 byte che queste scelte vi piacciano o no, e il vostro unico compito è produrre byte che si decifrano nell'originale sotto quella procedura esatta. Una modalità più moderna, un IV che sembra innocuo, un contatore che parte dove gli viene naturale: ognuno di questi è un difetto nell'istante in cui diverge da ciò che il lettore si aspetta. L'interoperabilità contro una specifica congelata non è approssimativa. È esatta al byte oppure è rotta

Per questo motivo la convalida del verifier non rappresenta un test sufficiente. Conferma solo che la derivazione della chiave sia corretta. Un test reale richiede di decrittografare il pacchetto e confrontare i byte recuperati con quelli originali, o verificare la rilettura delle celle dopo un ciclo di scrittura e crittografia. Il verifier conferma la password; solo il corpo del file dimostra la correttezza della crittografia

Modalità supportate per la gestione di file protetti

La superficie pubblica è piccola. Per scrivere una cartella di lavoro moderna protetta da password, popolate o aprite un TXLSXWorkbook e chiamate SaveAsEncrypted con un nome file e una password; serializza la cartella di lavoro ed esegue la pipeline di Standard Encryption descritta sopra, restituendo 1 in caso di successo. SaveAsEncryptedAgile prende gli stessi argomenti e scrive lo schema Agile che Excel 2010 e successivi usano per default, con derivazione della chiave SHA-512 e cifratura del pacchetto AES-256. Per leggere, chiamate CanReadEncrypted per verificare se un file è un contenitore Compound File, poi biforcate: OpenEncrypted decifra i pacchetti Standard e Agile e ripiega su Open per i file in chiaro, e solleva EXlsxEncryptionNotImplemented quando la password è sbagliata o il contenitore usa un altro schema. Un file .xls legacy è un Compound File anche lui, quindi va a TXLSWorkbook.Open con la password. La gestione della modalità e il ciclo di re-key descritti sopra stanno sotto queste chiamate; voi fornite password e nome file e il motore rispetta la specifica per conto vostro

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // Riapri lato consumatore
    Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
  finally
    Book.Free;
  end;
end;

La struttura dell'output protetto, lo stream EncryptionInfo, i blocchi verifier e il layout del pacchetto sono descritti nella nostra guida all'output XLSX protetto con AES. Per le questioni inerenti il blocco a livello di foglio e le relazioni con il layout di pagina e la stampa, consulta l'articolo su protezione, layout di pagina e stampa. Entrambi si basano sul percorso di crittografia descritto in questa pagina, incluso all'interno del componente per fogli di calcolo HotXLS per Delphi e C++Builder insieme alle API di lettura, scrittura e rendering trattate altrove in questo blog