Technical Article

Por que o Excel rejeita sua pasta de trabalho criptografada: ECB e RC4

Você grava uma pasta de trabalho, a criptografa com uma senha, entrega o arquivo a um colega e o colega a abre no Excel. O Excel solicita a senha. O colega a digita e o Excel a aceita. Até aí, a criptografia parece correta. Em seguida, o Excel apresenta uma caixa de diálogo informando que o arquivo está corrompido e não pode ser aberto, ou ele abre exibindo uma planilha de células sem sentido. A senha estava correta. O arquivo está quebrado de qualquer maneira. Este é o modo de falha mais desconcertante na criptografia do Office, porque a parte que informa que a senha está correta e a parte que armazena os dados são protegidas por duas operações diferentes, e acertar uma delas não garante a proteção da outra.

Ambos os bugs descritos aqui tinham exatamente esse formato. Em cada caso, o verificador passava mas o corpo não, o que faz você buscar por um bug de senha ou de derivação de chaves que não existe. A falha real estava adiante, na forma como os bytes do pacote foram transformados. As duas falhas são independentes, uma no caminho AES e outra no RC4, mas compartilham um problema de diagnóstico, por isso vale a pena entender por que um resultado parcialmente correto é o tipo mais difícil de interpretar.

Por que uma senha válida não garante a integridade do corpo

O formato que o XLSX criptografado moderno utiliza é o ECMA-376 Standard Encryption, e ele armazena duas coisas criptografadas lado a lado. Uma é o EncryptionVerifier: um pequeno bloco contendo um valor aleatório e o hash desse valor, criptografados com a chave derivada da senha. A outra é o EncryptedPackage: todo o contêiner zip da pasta de trabalho, criptografado com a mesma chave. O verificador existe para que o leitor confirme a senha antes de gastar processamento com megabytes do corpo. Descriptografe o verificador, calcule o hash do valor aleatório, compare-o com o hash armazenado e, se coincidirem, a senha está correta.

A armadilha é que o verificador e o pacote são criptografados por chamadas separadas em buffers separados. Uma chave derivada corretamente descriptografará o verificador corretamente, não importa o que aconteça ao pacote posteriormente. Portanto, se a sua derivação de chave estiver certa, mas a transformação do pacote estiver errada, o Excel confirma a senha a partir do verificador e depois falha no corpo. O sintoma se apresenta como "senha correta, arquivo corrompido", direcionando a investigação para o caminho da senha, que é a única parte que nunca esteve quebrada. A mesma separação governa o caso legacy do RC4: o hash do verificador é checado primeiro, e um corpo que sofra desvios ainda mantém essa checagem intacta.

Bug um: AES em ECB, não CBC

A especificação [MS-OFFCRYPTO] §2.3.4.15 detalha que a Criptografia Padrão (Standard Encryption) criptografa o pacote com AES no modo Electronic Codebook (ECB). Cada bloco de 16 bytes do pacote preenchido é criptografado de forma independente com a mesma chave. Não há encadeamento entre blocos e não há vetor de inicialização (IV). Essa é uma escolha incomum para os padrões modernos, onde o modo ECB é normalmente evitado, mas a interoperabilidade não é o espaço ideal para questionar as especificações. O Excel descriptografe o pacote como ECB, portanto o gerador deve criptografá-lo como ECB para que ambos estejam de acordo.

O bug era que o pacote estava sendo criptografado com AES no modo CBC usando um vetor de inicialização todo em zeros. Veja por que isso quase funciona, e por que o "quase" é o pior lugar onde pousar. Em CBC, o primeiro bloco de texto simples é submetido a um XOR com o IV antes da criptografia. Quando o IV é composto apenas por zeros, esse XOR não altera nada, de modo que o primeiro bloco de CBC com IV de zeros produz exatamente o mesmo texto cifrado que o ECB. A partir do segundo bloco, o CBC alimenta o texto cifrado anterior no seguinte, de modo que cada bloco após o primeiro diverge do ECB.

Agora considere a estrutura do arquivo. O layout do pacote posiciona um prefixo de comprimento de 8 bytes em little-endian logo no início, de modo que as partes do arquivo que o Excel checa mais cedo residem no primeiro ou segundo bloco. Um primeiro bloco que coincide por acaso significa que a validação inicial passa, enquanto cada bloco posterior descriptografe-se em ruído. A correção é direta: criptografe cada bloco de 16 bytes com ECB e pare o encadeamento. No mecanismo, a função XlsEncryptStdPackage percorre o buffer preenchido em etapas de 16 bytes e chama AESEncryptECB128Block em cada uma delas, que é o mesmo primitivo já usado para os blocos do verificador. O código-fonte carrega um comentário no loop que declara a regra de forma clara: o CBC com IV de zeros apenas coincide com o ECB para o primeiro bloco, portanto o restante do pacote se descriptografaria como lixo e o Excel o rejeitaria.

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;

Bug dois: o re-key do RC4 perde a sincronia

O caminho legacy do arquivo .xls usa o esquema RC4 CryptoAPI, e sua regra é diferente. O documento [MS-OFFCRYPTO] §2.3.6 especifica que a cifra recebe nova chave (re-key) a cada limite de bloco de 1024 bytes. O fluxo é dividido em blocos de 1024 bytes, uma nova chave RC4 é derivada para os blocos número 0, 1, 2 e assim por diante, e dentro de cada bloco o fluxo de chave é consumido continuamente de byte para byte. Dois invariantes devem ser mantidos juntos: re-key em cada limite e consumo do fluxo de chave sem lacunas dentro de um bloco. O RC4 é uma cifra de fluxo, portanto seu fluxo de chaves é uma sequência única ordenada; o n-ésimo byte que você extrai é determinado por quantos bytes foram extraídos antes dele. A descriptografia é o mesmo XOR contra a mesma sequência, o que significa que o gerador e o consumidor devem extrair exatamente os mesmos bytes nas mesmas posições.

Essa é toda a dificuldade. Uma cifra de fluxo não possui ressincronização. Se você desperdiçar um único byte de fluxo de chave, cada byte posterior será submetido a um XOR com o byte incorreto do fluxo de chaves, e o erro nunca se corrige sozinho; ele se propaga até o final do bloco e, uma vez que a posição de execução está incorreta, para cada bloco posterior. O bug aqui fazia exatamente isso. O contador de blocos iniciava a partir de um valor de sentinela de menos um, e a rotina de salto presumia que o contador já coincidia com o bloco atual. Partindo dessa sentinela, ela realizava o re-key e consumia um bloco de 1024 bytes de fluxo de chave que nunca deveria ter sido consumido e, no processo, tornava negativa a contagem restante. A partir desse ponto, o descriptografador ficava um bloco inteiro fora de fase. O verificador, checado antes de tudo isso, ainda passava, de modo que a senha parecia correta enquanto cada célula de dados retornava como lixo eletrônico.

A lógica corrigida reside em TXLSDecrypterRC4. Tanto a função Skip quanto a Decrypt compartilham um mesmo loop: re-key apenas quando a posição de execução cruzar para um novo bloco, onde o índice do bloco é a posição dividida por REKEY_BLOCK_SIZE (1024), consumindo então até o restante do bloco atual e nada mais. O MakeKey é chamado com o índice de bloco correto, nunca com um índice desatualizado ou de sentinela, e a posição avança no número exato de bytes processados para que Skip e Decrypt permaneçam alinhados com o gerador. A lição reside na menor unidade: um único byte desperdiçado não é um erro pequeno em uma cifra de fluxo, é uma perda total de tudo o que está adiante.

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;

Interoperabilidade com uma especificação fechada exige conformidade exata de bytes

Ambos os bugs se reduzem ao mesmo princípio básico, o qual vale a pena destacar porque muda a forma como você avalia as decisões de projeto. Quando o consumidor da sua saída é um programa externo fixo que você não pode alterar, o modo da cifra e o re-key não são detalhes de implementação que você pode otimizar ou simplificar. Eles fazem parte do contrato. O Excel descriptografará com ECB e re-key a cada 1024 bytes, quer você goste dessas escolhas ou não, e o seu único trabalho é produzir bytes que se descriptografem no original sob esse procedimento exato. Um modo que pareça mais moderno, um IV que pareça inofensivo, um contador que comece onde parece natural; qualquer um desses é um defeito no instante em que diverge do que o leitor espera. A interoperabilidade contra uma especificação fechada não é aproximada. Ela é exata em nível de bytes ou está quebrada.

É também por isso que o verificador é um teste fraco por si só. Ele diz que a derivação de chave funciona, o que é necessário, mas longe de ser suficiente. Um teste que apenas abre um arquivo criptografado e confirma se a senha passa relatará sucesso enquanto o corpo estiver ilegível. Um teste real descriptografa o pacote e compara os bytes recuperados com a entrada original, ou realiza um ciclo completo de criptografar e descriptografar e lê as células de volta. O verificador prova a senha; apenas o corpo prova a criptografia.

A forma suportada de ler e gravar pastas de trabalho protegidas

A interface pública é pequena. Para gravar uma pasta de trabalho moderna protegida por senha, preencha ou abra um TXLSXWorkbook e chame SaveAsEncrypted com o nome do arquivo e a senha; ela serializa a pasta de trabalho e executa o pipeline da Criptografia Padrão corrigido pela primeira solução, retornando 1 em caso de sucesso. Para ler, chame CanReadEncrypted para testar se um arquivo é um contêiner Compound File criptografado, e faça o desvio: o OpenEncrypted gerencia o caminho criptografado e recorre ao Open para arquivos normais, e o Open com senha está disponível diretamente. O manuseio de modos e o loop de re-key descritos acima funcionam sob essas chamadas; você fornece a senha e o nome do arquivo, e o mecanismo atende à especificação por você.

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;

O formato da saída protegida, o fluxo EncryptionInfo, os blocos de verificador e o layout do pacote são abordados em nosso tutorial de saída XLSX protegida por AES. Para a questão separada de bloqueio no nível da planilha e como a proteção interage com a configuração de página e impressão, consulte o artigo sobre proteção, configuração de página e impressão. Ambos se apoiam no caminho de criptografia descrito aqui, fornecido como parte do componente de planilha HotXLS para Delphi e C++Builder, junto com as APIs de leitura, gravação e renderização cobertas em outras partes deste blog.