O HotXLS lê arquivos do Excel com criptografia Agile — a proteção por senha aplicada por padrão pelo Excel 2010 e por todas as versões posteriores — por meio de uma única chamada: TXLSXWorkbook.OpenEncrypted. O componente analisa o descritor de criptografia XML, deriva chaves a partir da senha com uma cadeia de hashes SHA-512 por contagem de rotações (spin-count), verifica a senha contra o verificador criptografado e, em seguida, descriptografa o pacote em segmentos AES-CBC de 4096 bytes. Nenhuma instalação do Excel, COM ou DLL de criptografia externa está envolvida
Este artigo aborda especificamente o lado da leitura da criptografia Agile. Dois problemas correlacionados possuem seus próprios artigos: a interoperabilidade com os esquemas legados RC4 e XOR dentro dos arquivos antigos BIFF .xls é tratada no artigo de interoperabilidade ECB e RC4, e a produção de pastas de trabalho protegidas por senha com a Criptografia Padrão ECMA-376 é descrita no artigo de saída XLSX protegida por AES. Aqui o arquivo já existe, outra pessoa o criptografou e seu trabalho é abri-lo
O cenário que força essa questão é familiar para qualquer pessoa que gerencie um pipeline de documentos. Um serviço de importação no lado do servidor aceita uploads de pastas de trabalho; não há Excel na máquina e nunca haverá; e uma bela manhã um cliente faz o upload de um .xlsx perfeitamente comum que o leitor ZIP rejeita porque não se trata de um arquivo ZIP. O cliente salvou o arquivo com uma senha. A partir desse momento, ou o seu carregador entende o formato [MS-OFFCRYPTO] ou ele rejeita o arquivo de volta para um usuário que, do ponto de vista dele, não fez nada de incomum
O que é a criptografia Agile em um arquivo do Excel?
A criptografia Agile é o esquema de proteção por senha definido em [MS-OFFCRYPTO] §2.3.4.10 a §2.3.4.15, e é o que o Excel 2010 e posteriores gravam sempre que uma pasta de trabalho é salva com senha. O arquivo criptografado não é mais um pacote ZIP. É um contêiner OLE Compound File Binary (CFB) que abriga dois fluxos (streams): o EncryptionInfo, que descreve como a criptografia foi realizada, e o EncryptedPackage, que é o arquivo ZIP .xlsx real criptografado como um blob opaco. A assinatura CFB (D0 CF 11 E0 A1 B1 1A E1) é the same magic que arquivos legados BIFF .xls carregam, razão pela qual um arquivo renomeado ou criptografado não pode ser classificado apenas por sua extensão
O que diferencia a criptografia Agile de suas antecessoras é que o EncryptionInfo é autodescritivo. Após um prefixo de versão de 8 bytes, com versão principal e secundária iguais a 4, the stream é um descritor XML em UTF-8. Um elemento keyData declara a cifra (AES), o modo de encadeamento (ChainingModeCBC), o hash (SHA512), o comprimento da chave em bits, o tamanho do bloco e um sal (salt) em Base64. Um elemento keyEncryptor de senha carrega seu próprio sal, o spinCount e três cargas úteis (payloads) em Base64: encryptedVerifierHashInput, encryptedVerifierHashValue e encryptedKeyValue. O Excel grava AES-256 com uma contagem de rotações (spin count) de 100.000, mas o descritor tem permissão para declarar AES-128 ou AES-192, e o HotXLS respeita o que quer que o keyBits indique, em vez de assumir 256
Um único ponto de entrada para pastas de trabalho em texto simples, Standard e Agile
O método TXLSXWorkbook.OpenEncrypted lida com todos os três estados que um chamador pode encontrar: ZIP simples, criptografia Standard e criptografia Agile, de modo que os manipuladores de upload não precisem classificar os arquivos antes de carregá-los. O método primeiro inspeciona o arquivo: se não houver uma assinatura CFB, ele direciona para o caminho normal de Open e a senha é simplesmente ignorada. Se o arquivo for um contêiner CFB, ele tenta a Criptografia Standard ECMA-376 primeiro e, quando a assinatura de versão do EncryptionInfo for a Agile 4.4, despacha para o pipeline Agile. O valor de retorno é 1 em caso de sucesso, o mesmo contrato do 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;
A alternativa de retorno (fallback) para entradas não criptografadas importa mais do que parece. Um importador em lote que sempre chama o OpenEncrypted não precisa de ramificações no local da chamada: arquivos que nunca foram protegidos são carregados exatamente como antes, e arquivos que chegam criptografados são descriptografados no local e depois fornecidos ao leitor ZIP comum como um fluxo em memória. Existe apenas um caminho de código para testar, não três
Como uma senha se torna uma chave AES?
A criptografia Agile nunca usa a senha diretamente. O HotXLS primeiro calcula um hash iterado: o resumo (digest) inicial é SHA-512 sobre o sal da senha concatenado com os bytes UTF-16LE da senha, e então o resumo é recalculado com hash por spinCount vezes, a cada rodada precedendo o contador de iterações de 32 bits little-endian ao resumo anterior. Com a contagem de rotações padrão de 100.000 do Excel, são cem mil chamadas sequenciais de SHA-512 por tentativa de senha, e esse é o ponto principal. A contagem de rotações é um limitador de força bruta: custa a um chamador legítimo alguns milissegundos uma única vez, e custa a um invasor por dicionário os mesmos milissegundos para cada tentativa de adivinhação
// [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 obtido após as iterações ainda não é uma chave. Três chaves distintas são derivadas dele aplicando hash mais uma vez com uma chave de bloco fixa de 8 bytes anexada, sendo uma constante para cada finalidade: FE A7 D2 76 3B 4B 9E 79 para descriptografar 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 real do pacote. Cada resultado do SHA-512 é truncado para o comprimento de chave declarado e, conforme 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 se aplica quando o sal da senha é estendido para o tamanho do bloco para uso como vetor de inicialização (IV) do CBC
Verificação de senha e a armadilha de truncamento do saltSize
O HotXLS verifica a senha antes de tocar no pacote, usando o par verificador obtido a partir do descritor. Ele descriptografa o encryptedVerifierHashInput com a primeira chave derivada, calcula o hash do resultado com SHA-512, descriptografa o encryptedVerifierHashValue com a segunda chave derivada e compara os dois resumos byte por byte. Uma divergência significa que a senha está incorreta, o que é relatado como um resultado distinto em vez de uma pasta de trabalho corrompida, e de forma crítica significa que o corpo do pacote nunca é descriptografado com uma chave incorreta, de modo que não há cenário em que uma senha incorreta produza dados corrompidos com aparência plausível
Existe um detalhe da especificação aqui 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 sal do criptografador de chave, e não o tamanho do bloco da cifra. Como o texto cifrado AES-CBC é alinhado por bloco, a entrada do verificador descriptografada retorna preenchida para um múltiplo de 16 bytes e deve ser truncada de volta para saltSize antes do cálculo do hash. O Excel sempre grava o saltSize igual ao blockSize, ambos como 16, portanto uma implementação que ignore o truncamento passará em todos os testes contra a saída real do Excel e depois falhará no primeiro arquivo de um produtor que escolheu um comprimento de sal diferente. O HotXLS trunca para o comprimento do sal porque é isso que a especificação realmente diz, e o fato de os dois valores coincidirem na prática é uma coincidência, não um contrato
Como o EncryptedPackage é descriptografado?
O fluxo EncryptedPackage começa com um tamanho de texto simples em little-endian de 8 bytes, seguido pelo texto cifrado em segmentos de 4096 bytes, e o HotXLS o descriptografa segmento por segmento com um IV novo por segmento. A chave do pacote em si não é derivada da senha: é uma chave intermediária aleatória que o gerador criptografou em encryptedKeyValue, e o HotXLS a extrai com a terceira chave derivada, truncando para o comprimento de chave declarado por keyData. O IV de cada segmento é o SHA-512 sobre o sal do keyData concatenado com o índice do segmento em little-endian de 32 bits, truncado para o tamanho do bloco. Essa construção significa que qualquer segmento de 4096 bytes possa ser descriptografado de forma independente, o que em princípio também torna o formato amigável ao acesso aleatório, embora o HotXLS descriptografe todo o pacote na memória e entregue os bytes ZIP resultantes para o seu carregador XLSX normal
O tamanho declarado do texto simples faz a parte final do trabalho. A saída do AES-CBC é alinhada por bloco, portanto o último segmento carrega até 15 bytes de preenchimento que não fazem parte do documento; o buffer descriptografado é truncado para o prefixo do tamanho, e o resultado é exatamente o ZIP .xlsx que o Excel criptografou. O HotXLS valida o prefixo contra o comprimento real do fluxo antes de descriptografar, de modo que um upload truncado ou um campo de tamanho adulterado falhe de forma limpa em vez de estourar limites
Relatório de erros e limites reais
Os modos de falha são deliberadamente mantidos separados. Uma senha incorreta gera uma exceção com uma mensagem explícita de senha errada, motivada pela divergência do verificador, para que uma interface de usuário possa solicitar que o usuário tente novamente. Um contêiner CFB cujo descritor declare algoritmos fora do conjunto suportado — qualquer coisa diferente de AES com encadeamento CBC e hash SHA-512 em um descritor Agile, ou um contêiner que não seja Standard nem Agile — gera uma exceção diferente identificando o esquema como não suportado. Os dois cenários nunca devem ser confundidos: tentar novamente uma senha contra um esquema não suportado desperdiça o tempo do usuário, e relatar uma senha incorreta como um erro de formato envia sua equipe de suporte pelo caminho errado
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 a pena expor os limites claramente. O HotXLS lê descritores Agile que declaram AES no modo CBC com SHA-512, cobrindo o que o Excel 2010 ao Excel 365 realmente gravam, em todos os três tamanhos de chave. Descritores que declarem outras cifras ou algoritmos de hash são rejeitados em vez de adivinhados, e criptografadores de chave baseados em certificado não são consultados, apenas o criptografador de chave por senha é. No lado da gravação, o HotXLS atualmente produz a Criptografia Standard em vez da Agile, uma distinção que importa se as ferramentas subsequentes inspecionarem o esquema; os detalhes estão no artigo sobre gravação de saída XLSX protegida por AES
Uploads protegidos por senha deixam de ser um caso especial quando o carregador trata a criptografia como parte do formato do arquivo, e não como uma exceção a ele. O ponto de entrada OpenEncrypted, a derivação de contagem de rotações (spin-count) SHA-512 e o pipeline segmentado AES-CBC descritos aqui são fornecidos como parte do HotXLS Delphi Excel Component, junto com o restante de seu mecanismo nativo de leitura e gravação de XLS e XLSX para Delphi e C++Builder