Escribe un libro de trabajo, lo cifra con una contraseña, le entrega el archivo a un colega y este lo abre en Excel. Excel solicita la contraseña. El colega la ingresa y Excel la acepta. Hasta allí, el cifrado parece correcto. Luego, Excel muestra un cuadro de diálogo que indica que el archivo está corrupto y no se puede abrir, o bien se abre mostrando una hoja de celdas sin sentido. La contraseña era correcta. Sin embargo, el archivo está roto. Este es el modo de falla más desconcertante en el cifrado de Office, porque la parte que le indica que la contraseña es correcta y la parte que contiene sus datos están protegidas por dos operaciones diferentes, y realizar una correctamente no garantiza en absoluto el éxito de la otra.
Ambos errores descritos aquí tenían exactamente esta forma. En cada caso, el verificador pasaba y el cuerpo no, lo que le lleva a buscar un error en la contraseña o en la derivación de clave que no existe. La falla real se encontraba más adelante, en cómo se transformaban los bytes del paquete. Los dos fallos son independientes (uno en la ruta AES y otro en la ruta RC4), pero comparten un problema de diagnóstico, por lo que vale la pena comprender por qué un resultado correcto a medias es el tipo más difícil de descifrar.
Por qué una contraseña exitosa no demuestra nada sobre el cuerpo
El formato que utiliza el XLSX cifrado moderno es el Cifrado Estándar (Standard Encryption) ECMA-376, y almacena dos elementos cifrados lado a lado. Uno es el EncryptionVerifier: un pequeño bloque que contiene un valor aleatorio y el hash de ese valor, cifrado con la clave derivada de la contraseña. El otro es el EncryptedPackage: el contenedor zip completo del libro de trabajo, cifrado con la misma clave. El verificador existe para que un lector pueda confirmar una contraseña antes de destinar recursos a los megabytes del cuerpo del documento. Descifre el verificador, obtenga el hash del valor aleatorio, compárelo con el hash almacenado y, si coinciden, la contraseña es correcta.
La trampa es que el verificador y el paquete se cifran mediante llamadas independientes sobre búferes separados. Una clave que se deriva correctamente descifrará el verificador de manera correcta, independientemente de lo que suceda con el paquete a continuación. Por lo tanto, si su derivación de clave es correcta pero la transformación del paquete es incorrecta, Excel confirmará la contraseña desde el verificador y luego fallará en el cuerpo del documento. El síntoma se lee como "contraseña correcta, archivo roto", lo que orienta la investigación hacia la ruta de la contraseña, que es la única parte que nunca estuvo rota. La misma separación rige el caso heredado de RC4: el hash del verificador se comprueba primero, y un cuerpo que se desincroniza sigue dejando esa comprobación intacta.
Error uno: AES en ECB, no CBC
La norma [MS-OFFCRYPTO] §2.3.4.15 especifica que el Cifrado Estándar cifra el paquete con AES en modo Electronic Codebook (ECB). Cada bloque de 16 bytes del paquete con relleno (padded) se cifra de manera independiente con la misma clave. No hay encadenamiento entre bloques ni vector de inicialización. Esta es una elección inusual para los estándares modernos, donde normalmente se evita ECB, pero la interoperabilidad no es el lugar para cuestionar la especificación. Excel descifra el paquete como ECB, por lo que un productor debe cifrarlo como ECB o las dos partes no coincidirán.
El error consistía en que el paquete se cifraba con AES en modo CBC utilizando un vector de inicialización compuesto de puros ceros. He aquí por qué esto casi funciona, y por qué "casi" es el peor lugar para quedar. En CBC, el primer bloque de texto plano se somete a una operación XOR con el IV antes del cifrado. Cuando el IV es todo ceros, esa operación XOR no cambia nada, por lo que el primer bloque de CBC con IV de ceros produce exactamente el mismo texto cifrado que ECB. A partir del segundo bloque, CBC introduce el bloque de texto cifrado anterior en el siguiente, por lo que cada bloque posterior al primero diverge de ECB.
Ahora superponga eso sobre la estructura. El diseño del paquete coloca un prefijo de longitud de 8 bytes en formato little-endian al principio de todo, de modo 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 validación inicial pasa con éxito, mientras que cada bloque posterior se descifra como ruido. La solución no es compleja una vez que se identifica el modo: cifre cada bloque de 16 bytes con ECB y detenga el encadenamiento. En el motor, XlsEncryptStdPackage recorre el búfer con relleno en pasos de 16 bytes y llama a AESEncryptECB128Block en cada uno de ellos, que es la misma primitiva ya utilizada para los bloques del verificador. El código fuente contiene un comentario en el bucle que establece la regla claramente: CBC con un IV de ceros solo coincide con ECB para el primer bloque, por lo que el resto del paquete se descifraría como basura y Excel lo rechazaría.
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;
Error dos: el cambio de clave de RC4 se desincroniza
La ruta del archivo .xls heredado utiliza el esquema CryptoAPI de RC4, y su regla es de un tipo diferente. La norma [MS-OFFCRYPTO] §2.3.6 especifica que el cifrado cambia de clave en cada límite de bloque de 1024 bytes. El flujo se divide en bloques de 1024 bytes, se deriva una clave RC4 nueva para el bloque número 0, 1, 2, etcétera, y dentro de cada bloque el flujo de clave (keystream) se consume de manera continua de byte a byte. Se deben mantener dos condiciones invariables a la vez: cambiar la clave en cada límite y consumir el flujo de clave sin interrupciones dentro de un bloque. RC4 es un cifrado de flujo, por lo que su flujo de clave es una única secuencia ordenada; el n-ésimo byte que se extrae está determinado por cuántos bytes se han extraído antes de él. Descifrado es la misma operación 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 la dificultad. Un cifrado de flujo no tiene resincronización. Si se pierde un byte de flujo de clave, cada byte posterior se somete a una operación XOR con el byte de flujo de clave incorrecto, y el error nunca se corrige por sí solo; se propaga en cascada hasta el final del bloque y, una vez que la posición de ejecución es incorrecta, a cada bloque posterior. El error aquí hacía exactamente eso. El contador de bloques comenzaba desde un valor centinela de menos uno, y la rutina de salto asumía que el contador ya coincidía con el bloque actual. Comenzando desde ese centinela, cambiaba la clave y ejecutaba un bloque de flujo de clave completo de 1024 bytes que nunca debería haberse consumido y, en el proceso, volvía negativo el recuento restante. A partir de ese punto, el descifrador estaba desfasado un bloque completo. El verificador, comprobado antes de todo esto, seguía pasando, por lo que la contraseña parecía correcta mientras que cada celda de datos resultaba en basura.
La lógica corregida reside en TXLSDecrypterRC4. Tanto Skip como Decrypt comparten un mismo bucle: cambiar de clave únicamente cuando la posición en ejecución 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 no más. Se llama a MakeKey con el índice del bloque, nunca con un índice desfasado o centinela, y la posición avanza por el número exacto de bytes procesados de modo que Skip y Decrypt permanezcan alineados en 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 cifrado de flujo, representa una pérdida total de todo el flujo posterior.
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;
La interoperabilidad con una especificación inalterable exige coincidir al byte
Ambos errores se reducen al mismo principio fundamental, y vale la pena mencionarlo por separado porque cambia la manera de evaluar las decisiones de diseño. Cuando el consumidor de su salida es un programa externo inalterable, el modo de cifrado y el intervalo para cambiar de clave no son detalles de implementación que pueda optimizar o simplificar. Son parte del contrato de comunicación física. Excel descifrará con el modo ECB y cambiará de clave en límites de 1024 bytes, sean o no de su agrado esas elecciones, y su única tarea es producir bytes que se descifren al original bajo ese procedimiento exacto. Un modo más moderno o un IV que parezca inofensivo representa un defecto en el instante en que diverge de lo que el lector espera. La interoperabilidad contra una especificación inalterable no es aproximada: es exacta al byte o está rota.
Esta es también la razón por la que el verificador es una mala prueba rápida por sí sola. Indica que la derivación de clave funciona (lo cual es necesario pero lejos de ser suficiente). Una prueba real descifra el paquete y compara los bytes recuperados con la entrada original, o procesa de ida y vuelta un libro de trabajo a través del cifrado y descifrado y lee las celdas de vuelta. El verificador demuestra la contraseña; solo el cuerpo demuestra el cifrado.
La forma admitida de leer y escribir libros de trabajo protegidos
La superficie pública es pequeña. Para escribir un libro de trabajo moderno protegido por contraseña, complete o abra un TXLSXWorkbook y llame a SaveAsEncrypted con un nombre de archivo y una contraseña; este serializa el libro de trabajo y ejecuta la canalización de Cifrado Estándar que corrigió la primera solución, devolviendo 1 en caso de éxito. Para leer, llame a CanReadEncrypted para verificar si un archivo es un contenedor Compound File cifrado, luego bifurque la lógica: OpenEncrypted maneja la ruta cifrada y recurre a Open para archivos planos, y Open con contraseña está disponible directamente. El manejo del modo y el bucle para cambiar de clave descritos anteriormente residen debajo de estas llamadas; usted suministra la contraseña y el nombre del archivo y el motor cumple la especificación en su nombre.
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;
La estructura de la salida protegida, el flujo EncryptionInfo, los bloques del verificador y el diseño del paquete se cubren en nuestra guía sobre salida XLSX protegida por AES. Para la cuestión independiente del bloqueo a nivel de hoja y cómo interactúa la protección con la configuración de página y la impresión, consulte el artículo sobre protección, configuración de página e impresión. Ambos se basan en la ruta de cifrado descrita aquí, que 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.