HotXLS lee archivos de Excel con cifrado Agile, la protección con contraseña que aplican de forma predeterminada Excel 2010 y todas las versiones posteriores, a través de una sola llamada: TXLSXWorkbook.OpenEncrypted. El componente analiza el descriptor de cifrado XML, deriva las claves a partir de la contraseña con una cadena de hash SHA-512 por conteo de iteraciones, verifica la contraseña contra el verificador cifrado y luego descifra el paquete en segmentos AES-CBC de 4096 bytes. No se requiere ninguna instalación de Excel, ni COM, ni DLL criptográficas externas
Este artículo cubre específicamente el lado de la lectura del cifrado Agile. Dos problemas cercanos tienen sus propios artículos: la interoperación con los esquemas heredados RC4 y XOR dentro de los archivos antiguos BIFF .xls se cubre en el artículo sobre interoperabilidad de ECB y RC4, y la producción de libros de trabajo protegidos con contraseña mediante el cifrado estándar ECMA-376 se cubre en el artículo sobre salida XLSX protegida por AES. Aquí el archivo ya existe, alguien más lo cifró y su trabajo es abrirlo
El escenario que obliga a abordar este asunto es familiar para cualquiera que administre un flujo de trabajo de documentos. Un servicio de importación del lado del servidor acepta cargas de libros de trabajo; no hay Excel en el equipo y nunca lo habrá; y una mañana un cliente carga un archivo .xlsx perfectamente ordinario que el lector ZIP rechaza porque no es un ZIP en absoluto. El cliente lo guardó con una contraseña. Desde ese momento, su cargador comprende [MS-OFFCRYPTO] o devuelve el archivo a un usuario que, desde su punto de vista, no hizo nada inusual
¿Qué es el cifrado Agile en un archivo de Excel?
El cifrado Agile es el esquema de protección con contraseña definido en [MS-OFFCRYPTO] §2.3.4.10 a §2.3.4.15, y es el que Excel 2010 y versiones posteriores escriben cada vez que se guarda un libro de trabajo con una contraseña. El archivo cifrado ya no es un paquete ZIP. Es un contenedor de Archivo Compuesto Binario (CFB) de OLE que contiene dos flujos de datos: EncryptionInfo, que describe cómo se realizó el cifrado, y EncryptedPackage, que es el archivo ZIP .xlsx real cifrado como un blob opaco. La firma CFB (D0 CF 11 E0 A1 B1 1A E1) is la misma firma mágica que llevan los archivos antiguos BIFF .xls, razón por la cual un archivo renombrado o cifrado no puede clasificarse únicamente por su extensión
Lo que distingue a Agile de sus predecesores es que EncryptionInfo es autodescriptivo. Después de un prefijo de versión de 8 bytes, con versiones mayor y menor ambas en 4, el flujo es un descriptor XML UTF-8. Un elemento keyData declara el cifrado (AES), el modo de encadenamiento (ChainingModeCBC), the hash (SHA512), la longitud de la clave en bits, el tamaño del bloque y una sal Base64. Un elemento de contraseña keyEncryptor contiene su propia sal, el spinCount y tres cargas útiles Base64: encryptedVerifierHashInput, encryptedVerifierHashValue y encryptedKeyValue. Excel escribe AES-256 con un conteo de iteraciones (spin count) de 100,000, pero el descriptor puede declarar AES-128 o AES-192, y HotXLS respeta lo que indique keyBits en lugar de asumir 256
Un único punto de entrada para libros de trabajo de texto sin formato, estándar y Agile
TXLSXWorkbook.OpenEncrypted maneja los tres estados que puede encontrar un llamador: ZIP simple, cifrado estándar y cifrado Agile, por lo que los controladores de carga no necesitan clasificar los archivos antes de cargarlos. El método primero inspecciona el archivo: si no hay firma CFB, se deriva a la ruta normal de Open y la contraseña simplemente se ignora. Si el archivo es un contenedor CFB, intenta primero el cifrado estándar ECMA-376 y, cuando la firma de versión de EncryptionInfo es Agile 4.4, se deriva al flujo de trabajo Agile. El valor de retorno es 1 en caso de éxito, el mismo comportamiento que 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;
La alternativa para entradas no cifradas es más importante de lo que parece. Un importador por lotes que siempre llama a OpenEncrypted no necesita ramificaciones en el punto de llamada: los archivos que nunca estuvieron protegidos se cargan exactamente igual que antes, y los archivos que llegan cifrados se descifran en su lugar y luego se entregan al lector ZIP común como un flujo de datos en memoria. Hay una sola ruta de código que probar, no tres
¿Cómo se convierte una contraseña en una clave AES?
El cifrado Agile nunca utiliza la contraseña directamente. HotXLS primero calcula un hash iterado: el resumen inicial es SHA-512 sobre la sal de la contraseña concatenada con los bytes UTF-16LE de la contraseña, y luego se vuelve a aplicar el hash al resumen spinCount veces, anteponiendo en cada ronda el contador de iteración de 32 bits little-endian al resumen anterior. Con el conteo de iteraciones predeterminado de Excel de 100,000, eso representa cien mil invocaciones seriales de SHA-512 por intento de contraseña, y ese es todo el propósito. El conteo de iteraciones es un regulador de fuerza bruta: a un llamador legítimo le cuesta unos pocos milisegundos una sola vez, y a un atacante por diccionario le cuesta esos mismos milisegundos por cada intento
// [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;
El hash resultante de las iteraciones todavía no es una clave. Se derivan tres claves distintas a partir de él aplicando el hash una vez más con una clave de bloque fija de 8 bytes añadida al final, una constante para cada propósito: FE A7 D2 76 3B 4B 9E 79 para descifrar la entrada del verificador, D7 AA 0F 6D 30 61 34 4E para el hash del verificador y 14 6E 0B E7 AB AC D0 D6 para desenvolver la clave real del paquete. Cada resultado de SHA-512 se trunca a la longitud de clave declarada y, según [MS-OFFCRYPTO], se rellena con bytes 0x36 en el caso teórico de que el hash sea más corto que la clave. La misma regla de relleno 0x36 se aplica cuando la sal de la contraseña se extiende al tamaño del bloque para usarse como vector de inicialización de CBC
La verificación de la contraseña y la trampa de truncamiento de saltSize
HotXLS verifica la contraseña antes de tocar el paquete, utilizando el par verificador del descriptor. Descifra encryptedVerifierHashInput con la primera clave derivada, aplica el hash al resultado con SHA-512, descifra encryptedVerifierHashValue con la segunda clave derivada y compara los dos resúmenes byte por byte. Un desacuerdo significa que la contraseña es incorrecta, lo que se informa como un resultado distinto en lugar de un libro de trabajo dañado, y críticamente significa que el cuerpo del paquete nunca se descifra con una clave incorrecta, por lo que no hay escenario donde una contraseña incorrecta produzca datos dañados con apariencia plausible
Hay un detalle de la especificación aquí que es fácil de implementar de manera incorrecta. La norma [MS-OFFCRYPTO] §2.3.4.13 define al verificador como saltSize bytes de datos aleatorios, donde saltSize es la longitud de la sal del encriptador de claves, no el tamaño del bloque de cifrado. Debido a que el texto cifrado AES-CBC está alineado por bloques, la entrada del verificador descifrada se devuelve rellena a un múltiplo de 16 bytes y debe truncarse nuevamente a saltSize antes de aplicar el hash. Excel siempre escribe saltSize igual a blockSize, ambos en 16, por lo que una implementación que omita el truncamiento supera todas las pruebas contra salidas reales de Excel y luego falla en el primer archivo de un productor que elija una longitud de sal diferente. HotXLS trunca a la longitud de la sal porque eso es lo que realmente dice la especificación, y que ambos valores coincidan en la práctica es una coincidencia, no un contrato
¿Cómo se descifra el EncryptedPackage?
El flujo de datos EncryptedPackage comienza con una tamaño de texto sin formato little-endian de 8 bytes, seguido del texto cifrado en segmentos de 4096 bytes, y HotXLS lo descifra segmento por segmento con un IV nuevo por cada segmento. La clave del paquete en sí no se deriva de la contraseña: es una clave intermedia aleatoria que el escritor cifró en encryptedKeyValue, y HotXLS la desenvuelve con la tercera clave derivada, truncándola a la longitud de clave declarada por keyData. El IV de cada segmento es SHA-512 sobre la sal de keyData concatenada con el índice de segmento de 32 bits little-endian, truncado al tamaño del bloque. Esa construcción significa que cualquier segmento de 4096 bytes puede descifrarse de manera independiente, lo que también hace que el formato sea adecuado para el acceso aleatorio en principio, aunque HotXLS descifra todo el paquete en la memoria y entrega los bytes ZIP resultantes a su cargador normal de XLSX
El tamaño del texto sin formato declarado realiza la última parte del trabajo. La salida de AES-CBC está alineada por bloques, por lo que el último segmento contiene hasta 15 bytes de relleno que no forman parte del documento; el búfer descifrado se trunca al prefijo de tamaño y el resultado es exactamente el archivo ZIP .xlsx que Excel cifró. HotXLS valida el prefijo con la longitud real del flujo antes de descifrar, de modo que una carga truncada o un campo de tamaño alterado falle limpiamente en lugar de desbordarse
Informe de errores y límites honestos
Los modos de falla se mantienen separados deliberadamente. Una contraseña incorrecta genera una excepción con un mensaje explícito de contraseña incorrecta, impulsado por el desacuerdo del verificador, de modo que una interfaz de usuario pueda pedir al usuario que vuelva a intentarlo. Un contenedor CFB cuyo descriptor declara algoritmos fuera del conjunto admitido, cualquier cosa que no sea AES con encadenamiento CBC y hash SHA-512 en un descriptor Agile, o un contenedor que no sea ni estándar ni Agile, genera una excepción diferente que identifica el esquema como no compatible. Ambos casos nunca deben confundirse: volver a intentar una contraseña contra un esquema no compatible hace perder tiempo al usuario, y reportar una contraseña incorrecta como un error de formato envía a su equipo de soporte por el camino equivocado
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 exponer los límites claramente. HotXLS lee descriptores Agile que declaran AES en modo CBC con SHA-512, lo que cubre lo que Excel 2010 a Excel 365 escriben realmente, en los tres tamaños de clave. Los descriptores que declaran otros cifrados o algoritmos de hash se rechazan en lugar de intentar adivinarlos, y no se consultan los encriptadores de claves basados en certificados, solo se utiliza el encriptador de claves por contraseña. En el lado de la escritura, HotXLS actualmente produce cifrado estándar en lugar de Agile, una distinción que importa si las herramientas posteriores inspeccionan el esquema; los detalles se encuentran en el artículo sobre la escritura de salidas XLSX protegidas por AES
Las cargas protegidas por contraseña dejan de ser un caso especial una vez que el cargador trata el cifrado como parte del formato de archivo en lugar de una excepción a este. El punto de entrada OpenEncrypted, la derivación SHA-512 por conteo de iteraciones y el flujo de trabajo segmentado AES-CBC descrito aquí se distribuyen como parte del componente HotXLS Delphi Excel Component, junto con el resto de su motor nativo de lectura y escritura de XLS y XLSX para Delphi y C++Builder