Technical Article

Por qué Excel rechaza su libro cifrado: ECB y RC4

Usted escribe un libro, lo cifra con una contraseña, le entrega el archivo a un colega y este lo abre en Excel. Excel le pide la contraseña. El colega la escribe y Excel la acepta. Hasta aquí el cifrado parece correcto. Luego, Excel muestra un cuadro de diálogo que indica que el archivo está dañado y no se puede abrir, o se abre mostrando una hoja de celdas sin sentido. La contraseña era correcta. El archivo está roto de todos modos. Este es el modo de fallo 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 nada sobre la otra

Ambos errores descritos aquí tenían exactamente esta forma. En cada caso, el verificador pasó y el cuerpo no, lo que le lleva a buscar un error de contraseña o de derivación de clave que no existe. El fallo real estaba más abajo, en cómo se transformaron 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 ver por qué un resultado a medias correcto es el más difícil de interpretar

Por qué una contraseña que pasa no demuestra nada sobre el cuerpo

El formato que utiliza el XLSX cifrado moderno es el cifrado estándar ECMA-376 Standard Encryption, y almacena dos cosas cifradas una al lado de la otra. 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: todo el contenedor zip del libro, cifrado con la misma clave. El verificador existe para que un lector pueda confirmar una contraseña antes de gastar esfuerzo en megabytes de cuerpo. 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 separadas sobre búferes separados. Una clave que se deriva correctamente descifrará el verificador correctamente sin importar lo que le suceda al paquete después. Por lo tanto, si la derivación de la clave es correcta pero la transformación del paquete es incorrecta, Excel confirma la contraseña a partir del verificador y luego falla en el cuerpo. El síntoma se lee como "contraseña correcta, archivo roto", lo que dirige la investigación a la ruta de la contraseña, que es la única parte que nunca estuvo rota. La misma separación rige para el caso heredado de RC4: el hash del verificador se comprueba primero, y un cuerpo que se desvía de la sincronización sigue dejando esa comprobación intacta

Error uno: AES en ECB, no en CBC

La norma [MS-OFFCRYPTO] §2.3.4.15 especifica que el cifrado estándar Standard Encryption cifra el paquete con AES en modo de libro de códigos electrónico (Electronic Codebook, ECB). Cada bloque de 16 bytes del paquete rellenado se cifra 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 ECB, pero la interoperabilidad no es un lugar para dudar de la especificación. Excel descifra el paquete como ECB, por lo que un productor debe cifrarlo como ECB o los dos no estarán de acuerdo

El error consistió en que el paquete se cifró con AES en modo CBC utilizando un vector de inicialización con todos ceros. He aquí por qué eso casi funciona, y por qué casi es el peor lugar para terminar. En CBC, el primer bloque de texto plano se somete a una operación XOR con el IV antes del cifrado. Cuando el IV son 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 texto cifrado anterior en el siguiente, por lo que cada bloque después del primero diverge de ECB

Ahora superponga eso a la estructura. El diseño del paquete coloca un prefijo de longitud de 8 bytes en formato little-endian al principio del archivo, por lo que las partes que Excel comprueba antes se encuentran en el primer bloque o dos. Un primer bloque que resulta coincidir significa que la validación más temprana pasa mientras cada bloque posterior se descifra como ruido. La solución no es sutil una vez que se nombra el modo: cifrar cada bloque de 16 bytes con ECB y detener el encadenamiento. 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 utiliza para los bloques del verificador. El código fuente lleva 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: la regeneración de claves de RC4 se desvía de la fase

La ruta de la extensión heredada .xls utiliza el esquema RC4 CryptoAPI, y su regla es diferente en su naturaleza. La especificación [MS-OFFCRYPTO] §2.3.6 indica que el cifrado se vuelve a generar con una clave nueva 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., y dentro de cada bloque el flujo de claves se consume de forma continua de byte a byte. Se deben mantener dos invariantes juntas: regenerar clave en cada límite y consumir el flujo de claves sin espacios dentro de un bloque. RC4 es un cifrado de flujo, por lo que su flujo de claves es una única secuencia ordenada; el enésimo byte que extrae está determinado por cuántos bytes ha extraído antes. El 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 toda la dificultad. Un cifrado de flujo no tiene resincronización. Si desperdicia un byte de flujo de claves, cada byte posterior se somete a una operación XOR con el byte de flujo de claves incorrecto y el error nunca se corrige; se propaga en cascada hasta el final del bloque y, una vez que la posición de ejecución es incorrecta, a cada bloque después de él. El error aquí hizo exactamente eso. El contador de bloques comenzó desde un valor centinela de menos uno, y la rutina de salto asumió que el contador ya coincidía con el bloque actual. Comenzando desde ese centinela, volvió a generar la clave y consumió un bloque completo de 1024 bytes de flujo de claves que nunca debería haberse consumido, y en el proceso impulsó el recuento restante a negativo. A partir de ese punto, el descifrador estaba un bloque completo fuera de fase. El verificador, comprobado antes de todo esto, seguía pasando, por lo que la contraseña parecía correcta mientras cada celda de datos salía como basura

La lógica corregida reside en TXLSDecrypterRC4. Tanto Skip como Decrypt comparten un mismo bucle: volver a generar la clave solo cuando la posición de 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 obsoleto o centinela, y la posición avanza por el número exacto de bytes procesados para 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 pequeño error en un cifrado de flujo, es una pérdida total de todo lo 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 congelada consiste en coincidir al byte

Ambos errores se reducen al mismo principio fundamental, y vale la pena afirmarlo por sí mismo porque cambia cómo se sopesan las opciones de diseño. Cuando el consumidor de su salida es un programa externo fijo que no puede cambiar, el modo de cifrado y la cadencia de regeneración de claves no son detalles de implementación que pueda optimizar o simplificar. Son parte del contrato. Excel descifrará con ECB y volverá a generar claves en límites de 1024 bytes, le agraden o no esas elecciones, y su único trabajo es producir bytes que se descifren al original bajo ese procedimiento exacto. Un modo que sea más moderno, un IV que parezca inofensivo, un contador que comience donde parezca natural; cualquiera de estos es un defecto en el instante en que diverge de lo que el lector espera. La interoperabilidad frente a 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í sola. Le indica que la derivación de la clave funciona, lo cual es necesario pero dista mucho de ser suficiente. Una prueba que solo abre un archivo cifrado y confirma que la contraseña pasa informará de éxito mientras el cuerpo sea ilegible. Una prueba real descifra el paquete y compara los bytes recuperados con la entrada original, o procesa un libro completo a través de cifrar y descifrar 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 protegidos

La superficie pública es pequeña. Para escribir un libro 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 y ejecuta el canal Standard Encryption que corrigió la primera solución, devolviendo 1 en caso de éxito. Para leer, llame a CanReadEncrypted para probar si un archivo es un contenedor de archivos compuestos (OLE2) cifrado, luego bifurque: OpenEncrypted maneja la ruta cifrada y recurre a Open para archivos planos, y Open con una contraseña está disponible directamente. El manejo del modo y el bucle de regeneración de claves descritos anteriormente se encuentran 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 forma de la salida protegida, el flujo de EncryptionInfo, los bloques de verificación y el diseño del paquete se detallan en nuestra guía práctica de 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 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 distribuye como parte de la HotXLS spreadsheet component para Delphi y C++Builder, junto con las API de lectura, escritura y renderizado cubiertas en otras secciones de este blog