Artículo técnico

Por qué Excel rechaza su libro de trabajo encriptado: ECB y RC4

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

Ambos errores descritos aquí tenían exactamente esta forma. En cada caso el verificador fue exitoso 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 estaba en el flujo de trabajo posterior, en cómo se transformaron los bytes del paquete. Las dos fallas son independientes, una en la ruta de AES y otra en la de RC4, pero comparten un problema de diagnóstico, por lo 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

Error 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 consistía en que el paquete estaba encriptado 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(nil);
  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;

Error dos: el recambio de claves de 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, a cada byte posterior a él se le aplica XOR contra el byte del flujo de claves equivocado, y el error nunca se corrige solo; cae en cascada hasta el final del bloque y, una vez que la posición actual es errónea, hacia cada bloque después de él. El error aquí hizo exactamente eso. El contador de bloques comenzaba a partir de 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, cambió de clave y ejecutó un bloque completo de 1024 bytes de flujo de claves que nunca debió haberse consumido, y en el proceso volvió negativo el recuento restante. Desde ese punto, el desencriptador estaba completamente desfasado en un bloque. El verificador, comprobado antes de nada de esto, aún fue exitoso, por lo que la contraseña parecía correcta mientras que cada celda de datos salía como basura

La lógica corregida vive en TXLSDecrypterRC4. Tanto Skip como Decrypt comparten un bucle: cambiar de 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: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    // CanReadEncrypted verifica la firma de Compound File (OLE2) de modo que
    // pueda derivar antes de intentar un Open normal. OpenEncrypted
    // enruta los archivos simples hacia Open y maneja el contenedor encriptado.
    if Book.CanReadEncrypted('legacy.xls') then
      Book.OpenEncrypted('legacy.xls', 'S3cret!')
    else
      Book.Open('legacy.xls');
    // leer celdas aquí
  finally
    Book.Free;
  end;
end;

La interoperabilidad con una especificación congelada es coincidir en cada byte

Ambos errores 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, popule o abra un TXLSXWorkbook y llame a SaveAsEncrypted con un nombre de archivo y una contraseña; este serializa el libro y ejecuta el flujo de trabajo de Encriptación estándar 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 Compound File encriptado, luego derive: OpenEncrypted maneja la ruta encriptada y retrocede a Open para archivos simples, y Open con una contraseña está disponible directamente. El manejo del modo y el bucle de cambio de clave descritos anteriormente se ubican debajo de estas llamadas; usted suministra la contraseña y el nombre del archivo y el motor cumple con 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');
    // 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