Articolo tecnico

Leggere file Excel crittografati in modalità Agile in Delphi con HotXLS

HotXLS legge file Excel crittografati in modalità Agile (la protezione con password applicata per impostazione predefinita da Excel 2010 e da ogni versione successiva) attraverso una singola chiamata: TXLSXWorkbook.OpenEncrypted. Il componente analizza il descrittore di crittografia XML, deriva le chiavi dalla password con una catena di hash SHA-512 spin-count, verifica la password rispetto al verificatore crittografato e quindi decrittografa il pacchetto in segmenti AES-CBC da 4096 byte. Non è necessaria alcuna installazione di Excel, né COM o DLL di crittografia esterne

Questo articolo tratta specificamente il lato di lettura della crittografia Agile. Due problemi correlati hanno articoli dedicati: l'interoperabilità con i vecchi schemi RC4 e XOR all'interno dei vecchi file BIFF .xls è descritta nell'articolo sull'interoperabilità ECB e RC4, mentre la produzione di cartelle di lavoro protette da password con crittografia standard ECMA-376 è coperta nell'articolo sull'output XLSX protetto da AES. In questo scenario il file esiste già, è stato crittografato da terzi, e il tuo compito è aprirlo

Lo scenario che impone il problema è familiare a chiunque gestisca una pipeline di documenti. Un servizio di importazione lato server accetta caricamenti di cartelle di lavoro; non c'è Excel sulla macchina e non ci sarà mai; e una mattina un cliente carica un file .xlsx del tutto ordinario che il lettore ZIP rifiuta perché non è affatto uno ZIP. Il cliente lo ha salvato con password. Da quel momento il caricatore deve comprendere lo standard [MS-OFFCRYPTO] oppure rimanda il file a un utente che, dal suo punto di vista, non ha fatto nulla di insolito

Cos'è la crittografia Agile in un file Excel?

La crittografia Agile è lo schema di protezione tramite password definito in [MS-OFFCRYPTO] da §2.3.4.10 a §2.3.4.15, ed è ciò che Excel 2010 e versioni successive scrivono ogni volta che una cartella di lavoro viene salvata con password. Il file crittografato non è più un pacchetto ZIP. Si tratta di un contenitore OLE Compound File Binary (CFB) che ospita due flussi: EncryptionInfo, che descrive come è stata eseguita la crittografia, ed EncryptedPackage, che rappresenta il vero archivio ZIP .xlsx crittografato come un blob opaco. La firma CFB (D0 CF 11 E0 A1 B1 1A E1) corrisponde alla stessa segnatura dei vecchi file BIFF .xls, motivo per cui un file rinominato o crittografato non può essere classificato solo in base all'estensione

Ciò che distingue Agile dai suoi predecessori è che EncryptionInfo è auto-descrittivo. Dopo un prefisso di versione a 8 byte, con versione principale e secondaria entrambe impostate a 4, il flusso si presenta come un descrittore XML in formato UTF-8. Un elemento keyData dichiara il cifrario (AES), la modalità di concatenazione (ChainingModeCBC), l'algoritmo di hash (SHA512), la lunghezza della chiave in bit, la dimensione del blocco e un sale (salt) codificato in Base64. Un elemento keyEncryptor della password porta il proprio sale, lo spinCount, e tre payload Base64: encryptedVerifierHashInput, encryptedVerifierHashValue, ed encryptedKeyValue. Excel scrive AES-256 con uno spin count di 100.000, ma al descrittore è consentito dichiarare AES-128 o AES-192, ed HotXLS rispetta il valore di keyBits anziché assumere sempre 256

Un unico punto di ingresso per cartelle di lavoro in testo semplice, Standard e Agile

La funzione TXLSXWorkbook.OpenEncrypted gestisce tutti e tre gli stati in cui può trovarsi un file: ZIP semplice, crittografia Standard e crittografia Agile, eliminando la necessità per i gestori del caricamento di classificare i file prima di aprirli. Il metodo esamina prima il file: se non c'è una firma CFB, rimanda al normale percorso Open e la password viene ignorata. Se il file is un contenitore CFB, tenta prima la crittografia standard ECMA-376 e, quando la firma della versione di EncryptionInfo indica la crittografia Agile 4.4, reindirizza alla pipeline Agile. Il valore restituito è 1 in caso di successo, rispettando lo stesso contratto di Open

var
  Wb: TXLSXWorkbook;
begin
  Wb := TXLSXWorkbook.Create;
  try
    // Works for plain .xlsx, Standard-encrypted and
    // Agile-encrypted files alike
    if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
      Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
  finally
    Wb.Free;
  end;
end;

Il fallback per l'input non crittografato è più importante di quanto sembri. Un importatore batch che chiama sempre OpenEncrypted non richiede ramificazioni nel codice chiamante: i file che non sono mai stati protetti vengono caricati esattamente come prima, mentre quelli che arrivano crittografati vengono decrittografati sul posto e passati al normale caricatore ZIP come flusso in memoria. C'è un solo percorso di codice da testare, non tre

Come fa una password a diventare una chiave AES?

La crittografia Agile non utilizza mai la password in modo diretto. HotXLS calcola innanzitutto un hash iterato: il digest iniziale è SHA-512 sul sale della password concatenato con i byte UTF-16LE della password stessa, quindi il digest viene ricalcolato per un numero di volte pari a spinCount, anteponendo in ogni ciclo il contatore delle iterazioni little-endian a 32 bit al digest precedente. Con lo spin count predefinito di Excel pari a 100.000, si hanno centomila chiamate seriali a SHA-512 per ogni tentativo di inserimento della password, ed è proprio questo il fulcro. Lo spin count funge da acceleratore contro la forza bruta: richiede pochi millisecondi a un chiamante legittimo per una singola operazione, mentre costa gli stessi millisecondi a un attaccante basato su dizionario per ogni singolo tentativo

// [MS-OFFCRYPTO] iterated password hash:
//   H(0) = SHA-512(salt + UTF-16LE(password))
//   H(n) = SHA-512(LE32(n - 1) + H(n - 1)), repeated spinCount times
function AgilePasswordHash(const Password: WideString;
  const Salt: TBytes; SpinCount: Integer): TBytes;
var
  buf: TBytes;
  i: Integer;
begin
  Result := XlsSHA512(Concat(Salt, Utf16LEBytes(Password)));
  SetLength(buf, 4 + 64);
  for i := 0 to SpinCount - 1 do
  begin
    PutLE32(buf, 0, i);            // iteration counter, little-endian
    Move(Result[0], buf[4], 64);   // previous digest
    Result := XlsSHA512(buf);
  end;
end;

L'hash elaborato non è ancora una chiave. Da esso vengono derivate tre chiavi distinte calcolando nuovamente l'hash con l'aggiunta di una chiave di blocco fissa a 8 byte, una costante per ogni scopo: FE A7 D2 76 3B 4B 9E 79 per decrittografare l'input del verificatore, D7 AA 0F 6D 30 61 34 4E per l'hash del verificatore, e 14 6E 0B E7 AB AC D0 D6 per estrarre la chiave effettiva del pacchetto. Ciascun risultato SHA-512 viene troncato alla lunghezza della chiave dichiarata e, secondo [MS-OFFCRYPTO], completato con byte 0x36 nel caso teorico in cui l'hash sia più corto della chiave. La stessa regola di riempimento 0x36 si applica quando il sale della password viene esteso alla dimensione del blocco per l'uso come vettore di inizializzazione CBC

Verifica della password e la trappola del troncamento a saltSize

HotXLS verifica la password prima di accedere al pacchetto, utilizzando la coppia di verificatori del descrittore. Decrittografa encryptedVerifierHashInput con la prima chiave derivata, calcola l'hash del risultato con SHA-512, decrittografa encryptedVerifierHashValue con la seconda chiave derivata e confronta i due digest byte per byte. Una mancata corrispondenza indica che la password è errata, evento segnalato come esito distinto anziché come cartella di lavoro corrotta; aspetto fondamentale, ciò significa che il corpo del pacchetto non viene mai decrittografato con una chiave errata, eliminando scenari in cui una password errata possa produrre dati corrotti dall'aspetto verosimile

C'è un dettaglio delle specifiche che è facile sbagliare. [MS-OFFCRYPTO] §2.3.4.13 definisce il verificatore come saltSize byte di dati casuali, dove saltSize rappresenta la lunghezza del sale del cifratore della chiave, e non la dimensione del blocco del cifrario. Poiché il testo cifrato AES-CBC è allineato al blocco, l'input decrittografato del verificatore viene restituito con riempimento fino a un multiplo di 16 byte, e deve essere troncato di nuovo a saltSize prima del calcolo dell'hash. Excel scrive sempre saltSize uguale a blockSize, entrambi pari a 16, quindi un'implementazione che salta il troncamento supera tutti i test sui file reali di Excel ma fallisce con il primo file proveniente da un produttore che ha scelto una lunghezza del sale diversa. HotXLS esegue il troncamento alla lunghezza del sale perché questo è ciò che prescrive la specifica, e il fatto che i due valori coincidano nella pratica è solo un caso, non una regola

Come viene decrittografato l'EncryptedPackage?

Il flusso EncryptedPackage inizia con un valore a 8 byte little-endian che indica la dimensione del testo in chiaro, seguito dal testo cifrato in segmenti da 4096 byte; HotXLS lo decrittografa segmento per segmento con un IV specifico per ogni blocco. La chiave del pacchetto stessa non è derivata dalla password: è una chiave intermedia casuale che l'applicazione di scrittura ha cifrato in encryptedKeyValue, ed HotXLS la estrae con la terza chiave derivata, troncandola alla lunghezza dichiarata da keyData. L'IV di ogni segmento è SHA-512 sul sale di keyData concatenato con l'indice del segmento little-endian a 32 bit, troncato alla dimensione del blocco. Questa struttura implica che qualsiasi segmento da 4096 byte può essere decrittografato in modo indipendente, che in teoria rende il formato adatto per l'accesso casuale, sebbene HotXLS decrittografi l'intero pacchetto in memoria e passi i byte ZIP risultanti al normale caricatore XLSX

La dimensione del testo in chiaro dichiarata svolge l'ultimo passaggio di lavoro. L'output AES-CBC è allineato al blocco, quindi l'ultimo segmento contiene fino a 15 byte di riempimento che non fanno parte del documento; il buffer decrittografato viene troncato al prefisso della dimensione, e il risultato è esattamente il file ZIP .xlsx crittografato da Excel. HotXLS convalida il prefisso rispetto alla lunghezza effettiva del flusso prima della decrittografia, garantendo che un caricamento troncato o un campo di dimensione manomesso fallisca in modo corretto anziché generare un errore di overflow

Segnalazione degli errori e limiti effettivi

Le modalità di guasto sono intenzionalmente mantenute separate. Una password errata solleva un'eccezione con un messaggio esplicito di errore password, generato dalla mancata corrispondenza del verificatore, consentendo all'interfaccia utente di invitare l'utente a riprovare. Un contenitore CFB il cui descrittore dichiara algoritmi non supportati (ovvero diversi da AES con concatenazione CBC e hash SHA-512 in un descrittore Agile), o un contenitore che non è né Standard né Agile, solleva un'eccezione diversa identificando lo schema come non supportato. Le due cose non devono mai essere confuse: tentare di inserire nuovamente la password per uno schema non supportato fa perdere tempo all'utente, e segnalare una password errata come errore di formato fuorvia il team di supporto

function LoadUploadedWorkbook(const FileName: WideString;
  const Password: WideString; Wb: TXLSXWorkbook): Boolean;
begin
  Result := False;
  try
    Result := Wb.OpenEncrypted(FileName, Password) = 1;
  except
    on E: EXlsxEncryptionNotImplemented do
      // Raised for both a wrong password and an unsupported
      // scheme; E.Message states which, so log it verbatim and
      // only offer a password retry for the wrong-password case
      RejectUpload(FileName, E.Message);
  end;
end;

Vale la pena esporre chiaramente i limiti. HotXLS legge i descrittori Agile che dichiarano AES in modalità CBC con SHA-512, coprendo ciò che Excel da 2010 a Excel 365 effettivamente produce, in tutte e tre le dimensioni di chiave. I descrittori che dichiarano altri cifrari o algoritmi di hash vengono rifiutati anziché elaborati per ipotesi, e non vengono consultati i cifratori di chiavi basati su certificati, ma solo quello basato su password. Sul lato di scrittura, HotXLS produce attualmente la crittografia Standard anziché Agile, distinzione importante se gli strumenti a valle analizzano lo schema; i dettagli si trovano nell'articolo sulla scrittura di output XLSX protetti da AES

I caricamenti protetti da password smettono di essere un caso speciale una volta che il caricatore tratta la crittografia come parte del formato di file anziché come un'eccezione ad esso. Il punto di ingresso OpenEncrypted, la derivazione SHA-512 spin-count e la pipeline AES-CBC segmentata descritta qui sono distribuiti come parte di HotXLS Delphi Excel Component, insieme al resto dell'engine nativo di lettura e scrittura XLS e XLSX per Delphi e C++Builder