HotXLS lee archivos de Excel con cifrado Agile, la protección por contraseña que Excel 2010 y todas las versiones posteriores aplican de forma predeterminada, a través de una sola llamada: TXLSXWorkbook.OpenEncrypted. El componente analiza el descriptor de cifrado XML, deriva las claves de la contraseña con una cadena de hash SHA-512 con recuento de giros (spin-count), verifica la contraseña frente al verificador cifrado y luego descifra el paquete en segmentos AES-CBC de 4096 bytes. No interviene ninguna instalación de Excel, ni COM, ni DLL de cifrado externa
Este artículo cubre específicamente el lado de la lectura del cifrado Agile. Dos problemas relacionados tienen sus propios artículos: la interoperabilidad 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 por contraseña con 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 esto es familiar para cualquiera que ejecute una canalización de documentos. Un servicio de importación del lado del servidor acepta cargas de libros de trabajo; no hay Excel en la máquina y nunca lo habrá; y una mañana un cliente carga un archivo .xlsx perfectamente común que el lector de ZIP rechaza porque no es un ZIP en absoluto. El cliente lo guardó con una contraseña. A partir de 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 por contraseña definido en [MS-OFFCRYPTO] §2.3.4.10 a §2.3.4.15, y es lo 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 OLE Compound File Binary (CFB) que contiene dos flujos: EncryptionInfo, que describe cómo se realizó el cifrado, y EncryptedPackage, que es el ZIP .xlsx real cifrado como un bloque (blob) opaco. La firma CFB (D0 CF 11 E0 A1 B1 1A E1) is la misma magia que llevan los archivos BIFF .xls heredados, por lo que un archivo renombrado o cifrado no se puede clasificar solo 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 versión principal y secundaria 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 lleva su propia sal, el spinCount y tres cargas útiles Base64: encryptedVerifierHashInput, encryptedVerifierHashValue y encryptedKeyValue. Excel escribe AES-256 con un recuento de giros de 100,000, pero el descriptor puede declarar AES-128 o AES-192, y HotXLS respeta lo que diga keyBits en lugar de asumir 256
Un único punto de entrada para libros de texto plano, Standard y Agile
TXLSXWorkbook.OpenEncrypted maneja los tres estados que un llamador puede encontrar: ZIP plano, cifrado Standard y cifrado Agile, por lo que los controladores de carga no necesitan clasificar los archivos antes de cargarlos. El método primero examina el archivo: si no hay firma CFB, se remite a la ruta Open normal y la contraseña simplemente se ignora. Si el archivo es un contenedor CFB, prueba primero el cifrado estándar ECMA-376 y, cuando la firma de versión de EncryptionInfo es Agile 4.4, se envía a la canalización Agile. El valor de retorno es 1 en caso de éxito, el mismo contrato 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;
El fallback para entradas no cifradas importa más de lo que parece. Un importador por lotes que siempre llama a OpenEncrypted no necesita ramificaciones en el sitio de la llamada: los archivos que nunca estuvieron protegidos se cargan exactamente como antes, y los archivos que llegan cifrados se descifran en su lugar y luego se introducen en el cargador ZIP ordinario como un flujo 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 calcular el hash del resumen spinCount veces, añadiendo en cada ronda el contador de iteraciones de 32 bits little-endian al resumen anterior. Con el recuento de giros 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 recuento de giros es un acelerador de fuerza bruta: le cuesta a un llamador legítimo unos pocos milisegundos una vez y le cuesta a un atacante de diccionario los mismos milisegundos por cada suposición 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;
El spun hash sigue no siendo una clave. Se derivan tres claves distintas de él calculando de nuevo su hash con una clave de bloque de 8 bytes fija añadida, una constante por 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 desempaquetar la clave del paquete real. Cada resultado 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 donde el hash es 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 su uso como vector de inicialización CBC
Verificación de 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, calcula el hash del resultado con SHA-512, descifra encryptedVerifierHashValue con la segunda clave derivada y compara los dos resúmenes byte por byte. Una discrepancia significa que la contraseña es incorrecta, lo que se notifica como un resultado distinto en lugar de un libro de trabajo distorsionado, y de manera crítica significa que el cuerpo del paquete nunca se descifra con una clave incorrecta, por lo que no hay ningún escenario donde una contraseña incorrecta verifique datos dañados con apariencia plausible
Hay un detalle de la especificación aquí que es fácil de entender mal. La norma [MS-OFFCRYPTO] §2.3.4.13 define el verificador como saltSize bytes de datos aleatorios, donde saltSize es la longitud de la sal del cifrador 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 regresa rellena a un múltiplo de 16 bytes y debe truncarse de nuevo a saltSize antes de calcular el hash. Excel siempre escribe saltSize igual a blockSize, ambos 16, por lo que una implementación que omita el truncamiento supera todas las pruebas con la salida real de Excel y luego falla en el primer archivo de un productor que eligió 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 los dos valores coincidan en la práctica es una coincidencia, no un contrato
¿Cómo se descifra el EncryptedPackage?
El flujo EncryptedPackage comienza con un tamaño de texto plano little-endian de 8 bytes, seguido por el texto cifrado en segmentos de 4096 bytes, y HotXLS lo descifra segmento por segmento con un IV nuevo por 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 desempaqueta 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 little-endian de 32 bits, truncado al tamaño del bloque. Esa construcción significa que cualquier segmento de 4096 bytes se puede descifrar de forma independiente, que es también lo que 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 XLSX normal
El tamaño de texto plano declarado realiza la parte final 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 del tamaño y el resultado es exactamente el ZIP .xlsx que cifró Excel. HotXLS valida el prefijo frente a la longitud real del flujo antes de descifrar, por lo que una carga truncada o un campo de tamaño alterado falla limpiamente en lugar de desbordarse
Informes de errores y límites reales
Los modos de fallo se mantienen separados deliberadamente. Una contraseña incorrecta genera una excepción con un mensaje explícito de contraseña incorrecta, impulsado por la discrepancia del verificador, de modo que una interfaz de usuario pueda sugerir 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 Standard ni Agile) genera una excepción diferente que identifica el esquema como no compatible. Ambos nunca deben confundirse: volver a intentar una contraseña en un esquema no compatible hace perder el tiempo del usuario, y notificar 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 con claridad. 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 adivinarse, y no se consultan los cifradores de claves basados en certificados, solo el cifrador de claves de contraseña. En el lado de la escritura, HotXLS produce actualmente cifrado Standard en lugar de Agile, una distinción que importa si las herramientas descendentes inspeccionan el esquema; los detalles se encuentran en el artículo sobre la escritura de salida XLSX protegida 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 del archivo en lugar de como una excepción a este. El punto de entrada OpenEncrypted, la derivación por recuento de giros SHA-512 y la canalización segmentada AES-CBC que se describen aquí se distribuyen como parte de HotXLS Delphi Excel Component, junto con el resto de su motor nativo de lectura y escritura XLS y XLSX para Delphi y C++Builder