💡 Full working example available on GitHub: compare-encrypted-pdf-and-word-documents-dotnet

El método antiguo era doloroso

Dos revisiones de un contrato de suministro llegan a tu bandeja de entrada. Ambas están protegidas con contraseña, cada una con una contraseña diferente, y alguien necesita una copia marcada que muestre qué cambió. La biblioteca de comparación que tienes espera entrada en texto plano, por lo que el flujo de trabajo añade un paso: descifrar ambos archivos a una carpeta temporal, comparar las copias en texto plano y luego recordar eliminarlas. Esa carpeta temporal es ahora el eslabón más débil en un proceso que existe específicamente porque los documentos son sensibles.

Hay una segunda versión del mismo problema que es más fácil pasar por alto. Algunos equipos omiten la carpeta temporal y descifran en memoria, lo que resuelve la cuestión de la limpieza pero no la del formato: la API de descifrado difiere según el formato, de modo que admitir hojas de cálculo cifradas después de PDFs cifrados implica una segunda integración en lugar de una segunda línea de código.

El costo no está principalmente en la llamada de descifrado, sino en todo lo que la rodea. Las copias en texto plano deben escribirse en algún lugar, limpiarse en cada ruta de salida, incluidas las de error, y mantenerse fuera de copias de seguridad y volcados de fallos. Un diff generado de esa manera también llega sin protección por defecto, de modo que la salida de dos entradas cifradas se convierte en el único archivo de la cadena que cualquiera puede abrir.

El costo real del desvío de descifrado: un directorio temporal que contiene copias en texto plano de documentos que fueron cifrados por una razón, con una limpieza que debe ser correcta en cada ruta de error.

Hay una mejor manera

La comparación protegida con contraseña es una capacidad de GroupDocs.Comparison para .NET que abre archivos PDF, DOCX, XLSX y PPTX cifrados en su lugar y decide qué contraseña protege el resultado de la comparación. No hay paso de descifrado, ni intermedios en texto plano: la contraseña viaja con el documento dentro de la propia comparación, como una propiedad en LoadOptions.

Antes de comenzar, necesitarás:

  • .NET 8.0 SDK o posterior
  • GroupDocs.Comparison 26.9.0 (licencia temporal)
  • Dos documentos cifrados del mismo formato y sus contraseñas

Instala con un solo comando:

dotnet add package GroupDocs.Comparison

El nuevo método: documentos cifrados directamente en el comparador

El ejemplo a continuación compara dos PDFs cifrados: el origen se abre con 1234, el destino con 4321, y escribe un único archivo de resultado con los cambios fusionados en línea. Contraseñas deliberadamente diferentes, porque ahí es donde se oculta el primer error.

Paso 1 - Asignar a cada documento su propio LoadOptions

Un Comparer contiene una fuente y cualquier número de destinos, y cada documento lleva su propia protección. La contraseña de la fuente se pasa al constructor; la contraseña de cada destino se pasa a su propia llamada Add.

// One LoadOptions per document - the constructor's options unlock the
// source only, and never reach the targets.
using var comparer = new Comparer("source.pdf",
    new LoadOptions { Password = "1234" });
comparer.Add("target.pdf", new LoadOptions { Password = "4321" });

Este es el detalle que sorprende a la gente. Pasar un solo LoadOptions al constructor y esperar que cubra los destinos es la forma más común en que esto falla, y debido a cómo se cronometran los fallos, no se anuncia donde buscarlo.

Paso 2 - Decidir qué protege el resultado

CompareOptions.PasswordSaveOption elige la protección de la salida: None, Source, Target o User. El valor predeterminado es None, que convierte silenciosamente dos entradas cifradas en un resultado sin protección.

// Inline markup, and the result reuses the source document's password.
var options = new PdfCompareOptions
{
    DisplayMode = PdfCompareOptions.ComparisonDisplayMode.Inline,
    PasswordSaveOption = PasswordSaveOption.Source
};

comparer.Compare("Result/1-pdf-inline.pdf", options);

Puntos clave:

  • PasswordSaveOption: Source reutiliza la contraseña de la fuente en la salida. Elige User con SaveOptions.Password para asignar una nueva.
  • ComparisonDisplayMode: anidado dentro de PdfCompareOptions, que también ofrece SideBySide e Interleaved. WordCompareOptions declara su propio enum con el mismo nombre pero valores diferentes, por lo que el nombre simple no compilará; califícalo.

Paso 3 - Proteger la salida con una contraseña propia

Cuando el diff llega a revisores que no deben conocer ninguna de las contraseñas originales, PasswordSaveOption.User toma el valor de SaveOptions.Password en lugar de reutilizar una contraseña de entrada.

var compareOptions = new PdfCompareOptions
{
    DisplayMode = PdfCompareOptions.ComparisonDisplayMode.Inline,
    PasswordSaveOption = PasswordSaveOption.User
};
var saveOptions = new SaveOptions { Password = "5678" };

comparer.Compare("Result/4-own-password.pdf", saveOptions, compareOptions);

Ambos objetos se pasan a la sobrecarga de Compare con tres argumentos. Establecer SaveOptions.Password por sí solo no cambia nada; el valor del enum es lo que activa la contraseña del lado de guardado. El resultado de esta llamada se abre con 5678 y rechaza 1234.

¿Por qué mi try/catch alrededor del Comparer no captura una contraseña incorrecta?

Porque el constructor nunca abre el documento. Sólo registra la ruta, y lo mismo hace Add. Ambos documentos se leen cuando se ejecuta Compare, y allí es donde se lanza PasswordProtectedFileException con el mensaje Password is missing. Una contraseña incorrecta se comporta idénticamente: se acepta en silencio en el momento de la construcción y luego se rechaza más tarde en Compare.

Así que protege la llamada a la comparación, no el constructor. Lo descubrí a la fuerza, envolviendo la construcción en un try y observando cómo un archivo cifrado pasa sin problemas antes de fallar tres líneas después. El repositorio imprime cada etapa, lo que hace evidente el orden en una primera lectura:

using var comparer = new Comparer("source.pdf");   // succeeds
comparer.Add("target.pdf");                        // succeeds
comparer.Compare("Result/unreachable.pdf");        // throws here

Comparación lado a lado: Antes vs. Después

Antes (descifrar primero) Después (GroupDocs.Comparison)
Pasos del pipeline Descifrar ambos, comparar, eliminar copias temporales Comparar
Texto plano en disco Dos copias, limpieza en cada ruta de error Ninguno
Protección del resultado Un paso de re‑cifrado separado Un valor de PasswordSaveOption
Cobertura de formatos Herramientas de descifrado por formato Un LoadOptions.Password para PDF, DOCX, XLSX, PPTX
Código requerido Helper de descifrado más comparación 4 líneas

Las características de comparación no cambian con entrada cifrada. Los modos de visualización, páginas de resumen y detección de estilo se comportan exactamente como lo hacen con archivos en texto plano, porque la protección se maneja completamente en la capa de carga.

Esa capa es lo que hace que la cobertura de formatos sea barata. LoadOptions.Password es una propiedad string simple, y la misma propiedad desbloquea PDF, DOCX, XLSX y PPTX; el código de carga en el ejemplo de Word más abajo es carácter por carácter lo que usan los ejemplos de PDF. Sólo cambia la clase de opciones, y solo porque cada formato expone diferentes opciones de renderizado. Añadir soporte para hojas de cálculo cifradas a código que ya compara PDFs cifrados no cuesta nada en la ruta de carga.

Ejemplo del mundo real: revisión de contratos entre despachos de abogados

Un equipo legal recibe cada revisión de un acuerdo cifrada, con la contraseña rotada en cada intercambio para que una contraseña filtrada no exponga todo el historial. El socio revisor necesita un documento marcado por ronda, y bajo las normas de retención la copia marcada no puede quedar sin protección en un recurso compartido.

Dos configuraciones lo cubren. Cada documento se desbloquea con su propio LoadOptions, por lo que rotar contraseñas no requiere manejo especial, y PasswordSaveOption.User otorga a cada diff distribuido una contraseña propia, una que desbloquea la comparación y nada más.

// Word revisions, so the reviewing partner can accept or reject each edit.
var options = new WordCompareOptions
{
    DisplayMode = WordCompareOptions.ComparisonDisplayMode.Revisions,
    PasswordSaveOption = PasswordSaveOption.Source
};

using var comparer = new Comparer("round3.docx",
    new LoadOptions { Password = "1234" });
comparer.Add("round4.docx", new LoadOptions { Password = "4321" });
comparer.Compare("Result/redline.docx", options);

¿Qué más puedes hacer con GroupDocs.Comparison?

  • Comparar más de dos documentos protegidos: agrega varios destinos cifrados a una comparación, para formatos Word y presentaciones.
  • Generar revisiones nativas de Word: WordCompareOptions.ComparisonDisplayMode.Revisions escribe los cambios que un revisor acepta o rechaza directamente en Word.
  • Controlar la carga de recursos externos: bloquea o permite en lista blanca las referencias remotas que lleva un documento, otra salvaguarda de LoadOptions.
  • Generar una página de resumen: GenerateSummaryPage añade una visión general de cambios al documento resultante.

Conclusión

El desvío de descifrado nunca se trató de la comparación, sino de una biblioteca que no podía leer lo que tenías. Establecer LoadOptions.Password por documento elimina la carpeta temporal, las rutas de limpieza y el diff sin protección al final de la cadena. Tres decisiones son todo lo que queda: una contraseña por documento, un PasswordSaveOption explícito en lugar del valor predeterminado None, y manejo de errores alrededor de Compare donde realmente ocurre la falla.

¿Listo para automatizar tu flujo de trabajo de documentos?

Recursos adicionales