Artigo Técnico

Ler Ficheiros Excel com Encriptação Agile no Delphi com o HotXLS

O HotXLS lê ficheiros Excel com encriptação Agile — a proteção por palavra-passe que o Excel 2010 e todas as versões posteriores aplicam por padrão — através de uma única chamada: TXLSXWorkbook.OpenEncrypted. O componente analisa o descritor de encriptação XML, deriva chaves a partir da palavra-passe com uma cadeia de hash SHA-512 e spin-count, valida a palavra-passe contra o verificador encriptado e, em seguida, desencripta o pacote em segmentos AES-CBC de 4096 bytes. Não está envolvida qualquer instalação do Excel, COM ou DLL criptográfica externa

Este artigo aborda especificamente o lado da leitura da encriptação Agile. Dois problemas relacionados possuem os seus próprios artigos: a interoperabilidade com os esquemas legados RC4 e XOR dentro de ficheiros BIFF .xls antigos é descrita no artigo sobre interoperabilidade com ECB e RC4, e a produção de livros de trabalho protegidos por palavra-passe com a Standard Encryption ECMA-376 é abordada no artigo sobre saída XLSX protegida por AES. Neste caso, o ficheiro já existe, outra pessoa o encriptou, e a sua tarefa consiste em abri-lo

O cenário que força esta questão é familiar a qualquer pessoa que execute um fluxo de processamento de documentos. Um serviço de importação do lado do servidor aceita carregamentos de livros de trabalho; não existe Excel na máquina e nunca existirá; e, numa manhã, um cliente carrega um .xlsx perfeitamente normal que o leitor ZIP rejeita porque não se trata de todo de um ZIP. O cliente guardou-o com uma palavra-passe. A partir desse momento, o seu carregador compreende o [MS-OFFCRYPTO] ou devolve o ficheiro a um utilizador que, do seu ponto de vista, não fez nada de invulgar

O que é a encriptação Agile num ficheiro Excel?

A encriptação Agile é o esquema de proteção por palavra-passe definido nas secções §2.3.4.10 a §2.3.4.15 do [MS-OFFCRYPTO], e é o que o Excel 2010 e posteriores escrevem sempre que um livro de trabalho é guardado com uma palavra-passe. O ficheiro encriptado deixa de ser um pacote ZIP. Passa a ser um contentor OLE Compound File Binary (CFB) que aloja dois fluxos: EncryptionInfo, que descreve como a encriptação foi executada, e EncryptedPackage, que é o ficheiro ZIP .xlsx real encriptado como um blob opaco. A assinatura CFB (D0 CF 11 E0 A1 B1 1A E1) é o mesmo identificador mágico que os ficheiros BIFF .xls legados contêm, razão pela qual um ficheiro renomeado ou encriptado não pode ser classificado apenas pela extensão

O que distingue a encriptação Agile das suas predecessoras é que o EncryptionInfo é autodescritivo. Após um prefixo de versão de 8 bytes, com a versão principal e secundária ambas como 4, the fluxo consiste num descritor XML UTF-8. Um elemento keyData declara a cifra (AES), o modo de encadeamento (ChainingModeCBC), the hash (SHA512), o comprimento da chave em bits, o tamanho do bloco e um salt Base64. Um elemento de palavra-passe keyEncryptor transporta o seu próprio salt, o spinCount e três conteúdos Base64: encryptedVerifierHashInput, encryptedVerifierHashValue e encryptedKeyValue. O Excel escreve AES-256 com um spin count de 100.000, mas o descritor tem permissão para declarar AES-128 ou AES-192, e o HotXLS respeita o valor que for definido por keyBits em vez de assumir 256

Um único ponto de entrada para livros de trabalho de texto simples, Standard e Agile

O recurso de contingência (fallback) para entradas não encriptadas é mais importante do que parece. Um importador em lote que chame sempre OpenEncrypted não necessita de ramificações no ponto de chamada: os ficheiros que nunca foram protegidos carregam exatamente como antes, e os que chegam encriptados são desencriptados no local e depois fornecidos ao leitor ZIP comum como um fluxo em memória. Existe apenas um caminho de código para testar, e não três

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;

Como é que uma palavra-passe se torna numa chave AES?

A encriptação Agile nunca utiliza a palavra-passe diretamente. O HotXLS calcula primeiro um hash iterado: o resumo inicial é SHA-512 sobre o salt da palavra-passe concatenado com os bytes UTF-16LE da palavra-passe e, em seguida, o resumo é recalculado por hash spinCount vezes, com cada ronda a prefixar o contador de iterações little-endian de 32 bits ao resumo anterior. Com o spin count padrão do Excel de 100.000, isso representa cem mil invocações de SHA-512 sequenciais por tentativa de palavra-passe, o que constitui todo o propósito. O spin count funciona como um limitador de força bruta: custa a um chamador legítimo alguns milissegundos uma única vez, e custa a um atacante de dicionário os mesmos milissegundos para cada tentativa individual

// [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;

O hash gerado continua a não ser uma chave. Três chaves distintas são derivadas a partir dele, calculando o hash mais uma vez com uma chave de bloco fixa de 8 bytes anexada, uma constante por finalidade: FE A7 D2 76 3B 4B 9E 79 para desencriptar a entrada do verificador, D7 AA 0F 6D 30 61 34 4E para o hash do verificador e 14 6E 0B E7 AB AC D0 D6 para extrair a chave do pacote real. Cada resultado de SHA-512 é truncado para o comprimento da chave declarado e, de acordo com o [MS-OFFCRYPTO], preenchido com bytes 0x36 no caso teórico em que o hash seja mais curto do que a chave. A mesma regra de preenchimento 0x36 aplica-se quando o salt da palavra-passe é estendido para o tamanho do bloco para utilização como vetor de inicialização CBC

Validação da palavra-passe e a armadilha da truncagem saltSize

O HotXLS valida a palavra-passe antes de aceder ao pacote, utilizando o par verificador do descritor. Desencripta o encryptedVerifierHashInput com a primeira chave derivada, calcula o hash do resultado com SHA-512, desencripta o encryptedVerifierHashValue com a segunda chave derivada e compara os dois resumos byte a byte. Uma divergência significa que a palavra-passe está incorreta, sendo reportada como um resultado distinto em vez de um livro de trabalho corrompido, e, crucialmente, garante que o corpo do pacote nunca seja desencriptado com uma chave incorreta, evitando qualquer cenário onde uma palavra-passe errada produza dados corrompidos com aspeto plausível

Existe um detalhe da especificação que é fácil de errar. O [MS-OFFCRYPTO] §2.3.4.13 define o verificador como saltSize bytes de dados aleatórios, onde saltSize é o comprimento do salt do encriptador de chaves, e não o tamanho do bloco de cifra. Visto que o texto cifrado AES-CBC é alinhado por blocos, a entrada descodificada do verificador regressa preenchida para um múltiplo de 16 bytes e deve ser truncada de volta para saltSize antes do cálculo do hash. O Excel escreve sempre saltSize igual a blockSize, ambos 16, pelo que uma implementação que ignore a truncagem passará todos os testes com saídas reais do Excel e falhará no primeiro ficheiro de um gerador que escolha um comprimento de salt diferente. O HotXLS efetua a truncagem para o comprimento do salt porque é isso que a especificação realmente refere, e a concordância prática dos dois valores é uma coincidência, e não um contrato

Como é desencriptado o EncryptedPackage?

O fluxo EncryptedPackage começa com um tamanho de texto simples de 8 bytes em little-endian, seguido pelo texto cifrado em segmentos de 4096 bytes, e o HotXLS desencripta-o segmento a segmento com um IV novo por segmento. A própria chave do pacote não deriva da palavra-passe: é uma chave intermédia aleatória que o gerador encriptou em encryptedKeyValue, e o HotXLS extrai-a com a terceira chave derivada, truncando para o comprimento da chave declarado por keyData. O IV de cada segmento é SHA-512 sobre o salt do keyData concatenado com o índice do segmento little-endian de 32 bits, truncado para o tamanho do bloco. Essa construção significa que qualquer segmento de 4096 bytes pode ser desencriptado de forma independente, o que em princípio torna o formato propício para acesso aleatório, embora o HotXLS desencripte todo o pacote para a memória e entregue os bytes ZIP resultantes ao seu leitor XLSX normal

O tamanho de texto simples declarado realiza a etapa final. A saída AES-CBC é alinhada por blocos, pelo que o último segmento contém até 15 bytes de preenchimento que não fazem parte do documento; o buffer desencriptado é truncado para o prefixo do tamanho e o resultado é exatamente o ficheiro ZIP .xlsx que o Excel encriptou. O HotXLS valida o prefixo em relação ao comprimento real do fluxo antes de desencriptar, garantindo que um carregamento truncado ou um campo de tamanho adulterado falhe de forma limpa, sem causar transbordo (overrun)

Reporte de erros e limites reais

Os modos de falha são deliberadamente mantidos separados. Uma palavra-passe incorreta gera uma exceção com uma mensagem explícita de palavra-passe errada, baseada na divergência do verificador, para que uma interface de utilizador possa sugerir uma nova tentativa ao utilizador. Um contentor CFB cujo descritor declare algoritmos fora do conjunto suportado — qualquer outro que não AES com encadeamento CBC e hash SHA-512 num descritor Agile, ou um contentor que não seja Standard nem Agile — gera uma exceção diferente identificando o esquema como não suportado. Os dois nunca devem ser confundidos: tentar introduzir uma palavra-passe num esquema não suportado faz o utilizador perder tempo, e reportar uma palavra-passe incorreta como um erro de formato encaminha a sua equipa de suporte na direção errada

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;

Os limites merecem ser expostos claramente. O HotXLS lê descritores Agile que declarem AES em modo CBC com SHA-512, o que cobre o que o Excel 2010 até ao Excel 365 efetivamente escrevem, em todos os três tamanhos de chaves. Os descritores que declaram outras cifras ou algoritmos de hash são rejeitados em vez de se tentar a adivinhação, e os encriptadores de chaves baseados em certificado não são consultados, apenas o encriptador de chaves de palavra-passe. No lado da escrita, o HotXLS produz atualmente a Standard Encryption em vez da Agile, uma distinção relevante se as ferramentas seguintes inspecionarem o esquema; os detalhes encontram-se no artigo sobre a gravação de saídas XLSX protegidas por AES

Os carregamentos protegidos por palavra-passe deixam de ser um caso especial a partir do momento em que o carregador trata a encriptação como parte do formato de ficheiro, e não como uma exceção ao mesmo. O ponto de entrada OpenEncrypted, a derivação com spin-count SHA-512 e o processo segmentado AES-CBC aqui descritos são fornecidos como parte do HotXLS Delphi Excel Component, a par do resto do seu motor nativo de leitura e escrita XLS e XLSX para Delphi e C++Builder