Artículo técnico

Auditoría de cifrado y permisos de PDF en Delphi con PDF Library for Delphi

Un indicador de permiso no es un mecanismo de seguridad. El bit que dice «no copiar» reside en el mismo diccionario /Encrypt que la criptografía, lo que le da una apariencia de aplicación que no tiene, y en cuanto trata ambos como una misma cosa su auditoría empieza a producir respuestas erróneas. La única pregunta que merece hacerse a un PDF no es «¿está cifrado?». Es más concreta y más difícil: qué algoritmo, qué revisión del controlador de seguridad, cuál de las dos contraseñas se estableció, qué bits de permisos se declaran y qué partes del archivo toca realmente el cifrado. Un archivo puede estar formalmente cifrado y en la práctica abierto. Puede negarse a ser leído y dejar sus metadatos en texto plano. Puede restringir la impresión mediante un indicador que cualquier visor es libre de ignorar. Auditar un PDF significa resolver todas esas cuestiones por separado, y PDF Library for Delphi, el motor PDF de losLab para Delphi y C++Builder, expone cada una de ellas tanto mediante una API plana de identificadores enteros como mediante una capa de clases tipada

Qué registra realmente el diccionario /Encrypt

ISO 32000-1 §7.6 define la seguridad del documento mediante unas pocas entradas de diccionario, y PDF Library for Delphi las refleja una a una en el registro TPDFEncryption. La versión del filtro V y la revisión R seleccionan la familia de algoritmos. Length contiene el tamaño de la clave. Los bits de permisos están en P, las cadenas de validación de las contraseñas de propietario y usuario en O y U (con OE y UE añadidos para AES-256), un indicador EncryptMetadata las acompaña y otros tres campos nombran los filtros criptográficos aplicados respectivamente a cadenas, flujos y archivos incrustados

El valor de este registro es que no interpreta nada por usted. Devuelve el diccionario sin procesar y le deja extraer las conclusiones, que es exactamente lo que necesita una auditoría. El caso de texto plano dentro de contenido cifrado aparece en StringFilterIdentity y StreamFilterIdentity: cuando cualquiera de ellos es verdadero, los datos correspondientes atraviesan intactos el filtro Identity, sin importar qué informe el estado de cifrado del documento. Un escáner que se detiene en «hay un diccionario /Encrypt» declarará protegido un archivo cuyas cadenas y flujos están al descubierto. El mismo matiz rige los metadatos. Cuando EncryptMetadata es falso, el paquete XMP queda legible para cualquier indexador aunque el contenido de las páginas no lo esté, algo importante en cuanto sus reglas de enrutamiento se basen en un campo de título o autor

Diagrama de PDF Library for Delphi de los campos del diccionario /Encrypt del PDF mapeados a propiedades de auditoría de TPDFEncryption, incluidas las trampas del filtro criptográfico Identity
Cada entrada de /Encrypt se corresponde con un campo de TPDFEncryption, y los indicadores del filtro Identity revelan qué cadenas, flujos o metadatos permanecen legibles con independencia del estado de cifrado

Una breve comprobación de seguridad con la API plana

Para la mayoría de las canalizaciones, cuatro llamadas planas responden las preguntas cotidianas. LoadFromFile devuelve 1 si tiene éxito y, una vez abierto el documento, los inspectores de cifrado informan sobre su estado descifrado:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', UserPassword) <> 1 then
      raise Exception.Create('Open failed: wrong password or damaged file');
    Writeln('status    : ', PDF.EncryptionStatus);     // decrypted / encrypted / unknown
    Writeln('algorithm : ', PDF.EncryptionAlgorithm);  // RC4 vs AES family
    Writeln('strength  : ', PDF.EncryptionStrength);   // clase de longitud de clave
    Writeln('owner pw? : ', PDF.CheckPassword(CandidatePassword));
  finally
    PDF.Free;
  end;
end;

CheckPassword importa más de lo que sugiere su firma de una línea. PDF define dos contraseñas con facultades desiguales. La contraseña de usuario es necesaria para abrir el archivo. La contraseña de propietario concede plenos derechos y anula todos los bits de permisos. Los bytes en disco son idénticos en ambos casos, pero una sesión abierta con la contraseña de propietario puede hacer cosas que la sesión con contraseña de usuario no puede; por eso una auditoría que no registre qué credencial se presentó está registrando solo la mitad de la verdad. La capa de clases permite consultar esa distinción. TPDFDocument.HasUserPassword y HasOwnerPassword indican qué exige el archivo, mientras que IsUserPassword y IsOwnerPassword indican qué contraseña abrió realmente la sesión actual. Registre ese hecho. Nunca registre los valores de las contraseñas

La escala Strength, donde «AES-256» significa dos cosas

Las funciones planas Encrypt y EncryptFile aceptan un entero Strength con cinco valores significativos: 0 para RC4 de 40 bits, 1 para RC4 de 128 bits, 2 para AES de 128 bits legible desde Acrobat 7, 3 para AES de 256 bits introducido con Acrobat 9 y 4 para AES de 256 bits requerido por Acrobat X y posteriores

La parte interesante es que 3 y 4 se etiquetan ambos como AES-256 y no son el mismo esquema. Strength 3 corresponde a la revisión 5 del controlador de seguridad, un diseño provisional que Acrobat 9 distribuyó y que ISO nunca adoptó. Strength 4 corresponde a la revisión 6, cuya función de derivación de claves se reforzó y estandarizó en ISO 32000-2. Para un documento que cree hoy no hay razón para elegir 3 en lugar de 4. Para una auditoría, la diferencia es decisiva: una política que diga «AES-256 según ISO 32000-2» solo se cumple con R6, y un archivo R5 que se denomina AES-256 incumple esa política aunque supere una comprobación ingenua de resistencia. La capa de clases los mantiene separados por nombre, esAES256Bit para R5 frente a esAES256BitAcroX para R6, y la propiedad EncryptionAcroX responde a la cuestión de la revisión con un único booleano

Escalera de fortaleza del cifrado PDF desde RC4 de 40 bits hasta AES-256 revisión 5 frente a revisión 6 para auditorías Delphi
Los niveles 3 y 4 se denominan ambos AES-256, pero solo la revisión 6 satisface una política ISO 32000-2, de modo que las auditorías deben registrar la revisión del handler y no solo la etiqueta

Bits de permisos y su letra pequeña sobre la longitud de clave

EncodePermissions empaqueta ocho indicadores en el entero que esperan Encrypt y EncryptFile. Imprimir, copiar, cambiar y añadir notas forman el conjunto básico; rellenar campos, copiar para accesibilidad, ensamblar e imprimir a máxima calidad forman el conjunto ampliado. La letra pequeña, que la propia demostración de cifrado de la biblioteca expone claramente, es que los cuatro ampliados solo tienen efecto con una resistencia de 128 bits o superior. El indicador de impresión a máxima calidad se somete a la misma regla: bórrelo para forzar una impresión de baja resolución y un documento de 40 bits le ignorará, porque esa degradación también requiere cifrado de 128 bits o superior. Codifique una política de «solo impresión de baja resolución» en un archivo de 40 bits y todos los visores imprimirán de todos modos a máxima calidad

La pregunta más profunda es quién aplica cualquiera de esos bits, y la respuesta es nadie en quien pueda confiar. Los permisos son instrucciones para lectores conformes, no restricciones criptográficas. La clave de descifrado es idéntica tanto si se permite copiar como si se deniega, de modo que un conjunto de permisos restrictivo solo mantiene honestos a los visores honestos. Un lector que decide ignorar los bits no enfrenta ningún obstáculo criptográfico. Si la obligación es impedir la extracción en lugar de desaconsejarla, el archivo necesita una contraseña de usuario y el flujo de trabajo necesita controles de proceso a su alrededor; un informe de auditoría debe indicar bajo cuál de esos dos regímenes se encuentra realmente cada archivo, en vez de tratar un indicador de permiso como un candado

Establecer una política y demostrar que se aplicó

Aplicar cifrado a archivos existentes no exige cargarlos en el árbol de objetos. EncryptFile procesa la entrada a la salida en una sola llamada, y el bucle de auditoría vuelve a abrir el resultado para confirmar qué se escribió en el disco. La demostración de cifrado distribuida sigue la misma secuencia de escribir y después volver a leer:

var
  PDF: TPDFlib;
  R: Integer;
begin
  PDF := TPDFlib.Create;
  try
    R := PDF.EncryptFile('in.pdf', 'out.pdf', 'owner-secret', 'user-secret', 4,
      PDF.EncodePermissions(1, 0, 0, 0,    // print allowed; copy/change/notes denied
                            0, 0, 0, 1));  // conjunto extendido: solo impresión a calidad completa
    if (R = 1) and (PDF.LoadFromFile('out.pdf', 'user-secret') = 1) then
    begin
      Writeln('algorithm = ', PDF.EncryptionAlgorithm);
      Writeln('strength  = ', PDF.EncryptionStrength);
      Writeln('owner pw accepted: ', PDF.CheckPassword('owner-secret'));
    end;
  finally
    PDF.Free;
  end;
end;

Los equipos que trabajan en la capa de documento disponen de la misma operación con conjuntos tipados en lugar de empaquetado de bits, algo que supera una revisión de código con mucha menos necesidad de entrecerrar los ojos:

if not Doc.Encrypt('owner-secret', 'user-secret', esAES256BitAcroX,
  [ppCanPrint], [ppCanPrintFull]) then
  raise Exception.Create('Encryption failed');

En ambos casos, el paso de volver a leer no es una ceremonia opcional. Detecta los errores de despliegue que de otro modo aparecerían meses después en la máquina de un cliente: una compilación antigua de la biblioteca que degrada silenciosamente la resistencia solicitada, una ruta de salida que nunca se escribió porque el directorio era de solo lectura, un entero de permisos cuyos argumentos se pasaron en el orden equivocado. Los tres superan una prueba de humo local y fallan en producción; reabrir la salida convierte cada uno en una excepción que ve durante la ejecución que creó el archivo. GetEncryptionFingerprint devuelve un valor compacto que puede almacenar con el registro del trabajo, de modo que una comparación posterior puede indicar si dos salidas comparten la misma configuración de cifrado sin reabrir ninguna de ellas

Falsos positivos de auditoría que conviene programar

Algunos patrones empujan de forma fiable a los escáneres de seguridad a una conclusión errónea, y cada uno surge de reducir una pregunta de varias partes a una respuesta de sí o no. El filtro criptográfico Identity es el ejemplo más claro. Hay un diccionario /Encrypt, el archivo se declara cifrado y, sin embargo, las cadenas y los flujos atraviesan sin cambios el filtro Identity, de modo que el contenido real está en texto plano. Leer StringFilterIdentity y StreamFilterIdentity antes de declarar protegido nada es la solución

La separación de los metadatos es más sutil. EncryptMetadata puede diferir del resto del documento en ambos sentidos, dejando un archivo cifrado con un paquete XMP legible o, con menor frecuencia, lo contrario. «El archivo está cifrado» no dice nada sobre si sus metadatos lo están, lo que importa en cuanto un indexador o una regla de enrutamiento intenta usar el título. Los archivos incrustados añaden un tercer eje: PDF permite un filtro criptográfico específico solo para adjuntos, de modo que los adjuntos pueden ser la única parte cifrada de un documento abierto o la única parte en texto plano de uno cifrado. Capture las tres asignaciones de filtro como campos separados para cadenas, flujos y archivos incrustados, y ninguna de estas trampas podrá sorprenderle. Almacene un único booleano y la llamada errónea será solo cuestión de tiempo

PDF Library for Delphi: Flujo de auditoría que comprueba StringFilterIdentity, StreamFilterIdentity, EncryptMetadata y el filtro criptográfico de ficheros incrustados antes de calificar un PDF como protegido
Cuatro ejes independientes deciden si un archivo de apariencia cifrada está realmente sellado, y contraerlos en un solo booleano acaba clasificando mal algún archivo

Eliminar el cifrado y elegirlo para archivos nuevos

Una auditoría suele acabar con la decisión de eliminar la protección, y la mecánica no es el obstáculo en ese caso. DecryptFile(InputFileName, OutputFileName, Password) escribe una copia descifrada sin una carga completa, y Decrypt sobre el documento cargado hace lo mismo en memoria una vez que el archivo ya está abierto. Ambos requieren una contraseña válida; ninguno evita la criptografía. El verdadero umbral es de política, no de código, así que sus reglas de admisión deben declarar claramente cuándo se permite la eliminación y registrar la clase de contraseña que la autorizó, porque el paso técnico por sí mismo no deja rastro

La elección para una salida nueva es más limitada de lo que sugieren los cinco valores Strength. Use Strength 4, AES-256 revisión 6, salvo que deba abrir archivos en visores anteriores a Acrobat X. Strength 2, AES-128, es el mínimo pragmático para una flota de visores antiguos que no puede actualizarse. Las opciones RC4 de 0 y 1 existen para que pueda leer y auditar archivos históricos, no para producir nada nuevo con ellas; recurrir a ellas en un diseño de 2026 indica que algún requisito previo está obsoleto

El estado de cifrado alimenta directamente las decisiones de firma, ya que un banco de trabajo que valida y firma documentos necesita la misma disciplina de volver a leer en la que se apoya esta auditoría. Ese terreno se trata en el artículo sobre el banco de trabajo de cumplimiento y firma. Cuando un lote aplica EncryptFile a miles de documentos grandes, la guía de acceso directo para PDF grandes muestra cómo mantener estable el uso de memoria durante la ejecución. La referencia completa de la API de cifrado está en la página de producto de PDF Library for Delphi