Escribe un libro de trabajo, lo encripta con una contraseña, le entrega el archivo a un colega y el colega lo abre en Excel. Excel solicita la contraseña. El colega la teclea y Excel la acepta. Hasta el momento la encriptación parece correcta. Luego Excel muestra un cuadro de diálogo que dice que el archivo está dañado y no se puede abrir, o se abre mostrando una hoja con celdas sin sentido. La contraseña era correcta. De todos modos el archivo está roto. Este es el modo de fallo más desorientador en la encriptación de Office, porque la parte que le dice que la contraseña es correcta y la parte que guarda sus datos están protegidas por dos operaciones diferentes, y hacer una correctamente no garantiza nada para la otra
Ambas trampas descritas aquí tienen exactamente esta forma. En cada caso el verificador pasa y el cuerpo no, lo que lo envía a buscar un error de contraseña o de derivación de claves que no existe. La verdadera falla está más abajo, en cómo se transforman los bytes del paquete. Las dos trampas son independientes, una en la ruta de AES y otra en la de RC4, pero comparten un problema de diagnóstico, así que vale la pena ver por qué un resultado a medias correcto es el tipo más difícil de leer
Por qué una contraseña aceptada no demuestra nada sobre el cuerpo
El formato que usa el XLSX encriptado moderno es la Encriptación estándar de ECMA-376, el cual almacena dos cosas encriptadas lado a lado. Una es el EncryptionVerifier: un pequeño bloque que contiene un valor aleatorio y el hash de ese valor, encriptado con la clave derivada de la contraseña. La otra es el EncryptedPackage: el contenedor zip completo del libro de trabajo, encriptado con la misma clave. El verificador existe para que un lector pueda confirmar una contraseña antes de gastar esfuerzo en megabytes del cuerpo. Se desencripta el verificador, se hasea el valor aleatorio, se compara con el hash almacenado y, si coinciden, la contraseña es correcta
La trampa es que el verificador y el paquete están encriptados mediante llamadas separadas sobre búferes separados. Una clave que se deriva correctamente desencriptará el verificador correctamente sin importar lo que le suceda al paquete después. Por lo tanto, si su derivación de claves es correcta pero la transformación de su paquete es incorrecta, Excel confirma la contraseña desde el verificador y luego falla en el cuerpo. El síntoma se lee como "contraseña correcta, archivo roto", lo que dirige la investigación hacia la ruta de la contraseña, que es la única parte que nunca se rompió. La misma separación rige el caso heredado de RC4: primero se comprueba el hash del verificador, y un cuerpo que se desincroniza aún deja esa comprobación intacta
Trampa uno: AES en ECB, no en CBC
[MS-OFFCRYPTO] §2.3.4.15 especifica que la Encriptación estándar encripta el paquete con AES en modo Electronic Codebook (ECB). Cada bloque de 16 bytes del paquete rellenado se encripta de forma independiente con la misma clave. No hay encadenamiento entre bloques y no hay vector de inicialización. Esta es una elección inusual para los estándares modernos, donde normalmente se evita el ECB, pero la interoperabilidad no es un lugar para cuestionar la especificación. Excel desencripta el paquete como ECB, por lo que un productor debe encriptarlo como ECB o ambos no estarán de acuerdo
El error tentador es encriptar el paquete con AES en modo CBC utilizando un vector de inicialización totalmente lleno de ceros. He aquí por qué eso casi funciona, y por qué casi es el peor lugar en el que caer. En CBC, se aplica XOR al primer bloque de texto plano con el IV antes del cifrado. Cuando el IV son todo ceros, ese XOR no cambia nada, de modo que el primer bloque de CBC con IV cero produce exactamente el mismo texto cifrado que ECB. Del segundo bloque en adelante, CBC alimenta el bloque de texto cifrado anterior hacia el siguiente, por lo que todos los bloques posteriores al primero difieren de ECB
Ahora superponga eso sobre la estructura. El diseño del paquete coloca un prefijo de longitud little-endian de 8 bytes justo al principio, de manera que las partes del archivo que Excel comprueba primero se sitúan en el primer o segundo bloque. Un primer bloque que coincide significa que la primera validación pasa mientras que cada bloque posterior se desencripta en ruido. La solución no es sutil una vez que se nombra el modo: encriptar cada bloque de 16 bytes con ECB y dejar de encadenar. En el motor, XlsEncryptStdPackage recorre el búfer rellenado en pasos de 16 bytes y llama a AESEncryptECB128Block en cada uno, que es la misma primitiva que ya se usó para los bloques verificadores. El código fuente tiene un comentario en el bucle que establece la regla de forma simple: CBC con un IV cero solo coincide con ECB para el primer bloque, por lo que el resto del paquete se desencriptaría como basura y Excel lo rechazaría
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('report.xlsx');
// SaveAsEncrypted serializa el libro de trabajo, luego ejecuta el
// flujo de trabajo de Encriptación estándar ECMA-376: AES-128 ECB sobre el
// paquete según [MS-OFFCRYPTO] 2.3.4.15. Devuelve 1 en caso de éxito.
if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
raise Exception.Create('Encryption failed');
finally
Book.Free;
end;
end;
Trampa dos: el recambio de claves RC4 se desincroniza
La ruta .xls heredada usa el esquema RC4 CryptoAPI, y su regla es diferente en su clase. [MS-OFFCRYPTO] §2.3.6 especifica que se cambian las claves del cifrado en cada límite de bloque de 1024 bytes. El flujo se divide en bloques de 1024 bytes, se deriva una nueva clave RC4 para el bloque número 0, 1, 2, y así sucesivamente, y dentro de cada bloque el flujo de claves se consume continuamente byte por byte. Tienen que mantenerse juntos dos elementos invariantes: recambiar la clave en cada límite y consumir el flujo de claves sin interrupciones dentro de un bloque. RC4 es un cifrador de flujo, por lo que su flujo de claves es una única secuencia ordenada; el enésimo byte que usted extrae está determinado por cuántos bytes haya extraído antes de él. La desencriptación es el mismo XOR contra la misma secuencia, lo que significa que el productor y el consumidor deben extraer exactamente los mismos bytes en las mismas posiciones
Esa es toda la dificultad. Un cifrador de flujo no tiene resincronización. Si desperdicia un byte de flujo de claves, cada byte posterior se aplica XOR contra el byte equivocado, y el error nunca se corrige solo; cascada hasta el final del bloque y, una vez que la posición acumulada queda mal, hasta cada bloque posterior. Un desliz pequeño de contabilidad hace exactamente eso. Imagine un contador de bloques que arranca en un valor centinela de menos uno, y una rutina de salto que asume que el contador ya coincide con el bloque actual. Partiendo de ese centinela, recambia la clave y consume un bloque completo de 1024 bytes de flujo de claves que nunca debió consumirse, y en el proceso lleva el resto del conteo a negativo. Desde ese punto el desencriptador está un bloque completo desfasado. El verificador, que se comprueba antes de todo esto, sigue pasando, así que la contraseña parece correcta mientras cada celda de datos sale como basura
En HotXLS la lógica vive en TXLSDecrypterRC4. Tanto Skip como Decrypt comparten un bucle: recambiar la clave solo cuando la posición actual cruza a un nuevo bloque, donde el índice del bloque es la posición dividida por REKEY_BLOCK_SIZE (1024), luego consumir hasta el resto del bloque actual y nada más. Se llama a MakeKey con el índice del bloque, nunca con un índice caducado o centinela, y la posición avanza según la cantidad exacta de bytes procesados para que Skip y Decrypt se mantengan alineados de fase con el productor. La lección se encuentra en la unidad más pequeña: un solo byte desperdiciado no es un error menor en un cifrador de flujo, es una pérdida total de todo el flujo posterior
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
// Un .xls legado protegido con RC4 o RC4 CryptoAPI se desencripta dentro
// de Open: pase la contraseña y el desencriptador de flujo mantiene el
// ritmo de recambio de claves alineado con el productor.
Book.Open('legacy.xls', 'S3cret!');
// leer celdas aquí
finally
Book.Free;
end;
end;
La interoperabilidad con una especificación congelada es coincidir en cada byte
Ambas trampas se reducen al mismo principio básico, y vale la pena afirmarlo de forma independiente porque cambia cómo se sopesan las opciones de diseño. Cuando el consumidor de su salida es un programa externo fijo que no se puede cambiar, el modo de cifrado y el ritmo de recambio de claves no son detalles de implementación que pueda optimizar o simplificar. Son parte del contrato. Excel desencriptará con ECB y recambiará de clave en límites de 1024 bytes le gusten o no esas opciones, y su único trabajo es producir bytes que se desencripten hacia el original bajo ese procedimiento exacto. Un modo que sea más moderno, un IV que parezca inofensivo, un contador que comience donde se sienta natural; cualquiera de estos es un defecto en el instante en que diverge de lo que el lector espera. La interoperabilidad contra una especificación congelada no es aproximada. Es exacta al byte o está rota
Esta es también la razón por la que el verificador es una prueba de humo deficiente por sí solo. Le dice que la derivación de claves funciona, lo cual es necesario pero está lejos de ser suficiente. Una prueba que solo abre un archivo encriptado y confirma que la contraseña pasa reportará éxito mientras que el cuerpo es ilegible. Una prueba real desencripta el paquete y compara los bytes recuperados con la entrada original, o completa el ciclo de un libro de trabajo a través del proceso de encriptación y desencriptación y vuelve a leer las celdas. El verificador demuestra la contraseña; solo el cuerpo demuestra la encriptación
La forma admitida de leer y escribir libros protegidos
La superficie pública es pequeña. Para escribir un libro moderno protegido con contraseña, llene o abra un TXLSXWorkbook y llame a SaveAsEncrypted con un nombre de archivo y una contraseña; el método serializa el libro y ejecuta el pipeline de Standard Encryption descrito arriba, y devuelve 1 en caso de éxito. SaveAsEncryptedAgile toma los mismos argumentos y escribe el esquema Agile que Excel 2010 y posteriores usan por defecto, con derivación de claves SHA-512 y encriptación de paquete AES-256. Para leer, llame a CanReadEncrypted para comprobar si un archivo es un contenedor Compound File, y luego bifurque: OpenEncrypted desencripta paquetes Standard y Agile y hace fallback a Open para archivos planos, y lanza EXlsxEncryptionNotImplemented cuando la contraseña es incorrecta o el contenedor usa otro esquema. Un archivo .xls legado también es un Compound File, así que va a TXLSWorkbook.Open con la contraseña. El manejo de modos y el bucle de recambio de claves descritos arriba viven debajo de estas llamadas; usted proporciona la contraseña y el nombre de archivo, y el motor se ajusta a la especificación por usted
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('quarterly.xlsx');
Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
// Reabrir en el lado del consumidor
Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
finally
Book.Free;
end;
end;
La forma de la salida protegida, el flujo EncryptionInfo, los bloques verificadores y el diseño del paquete se cubren en nuestro recorrido por la salida XLSX protegida con AES. Para la cuestión separada del bloqueo a nivel de hoja y cómo interactúa la protección con la configuración de la página y la impresión, consulte el artículo sobre protección, configuración de la página e impresión. Ambos se basan en la ruta de encriptación que se describe aquí, la cual se incluye como parte del componente de hoja de cálculo HotXLS para Delphi y C++Builder junto con las API de lectura, escritura y renderizado cubiertas en otras partes de este blog