Artículo técnico

Flags de permisos PDF, el entero /P y la copia /Perms

Cifras un informe con una máscara pensada para permitirlo todo, $FFFFFFF0, y la pestaña Security de Acrobat lista imprimir y cambiar el documento como no permitido. /P es una palabra de flags de 32 bits sin signo numerada desde el bit 1, así que esa máscara limpia print (bit 3) y modify (bit 4); el valor de todo permitido es $FFFFFFFC, el punto de partida de TPdfEncryptOptions en PDFium Component

Cómo se dispone la palabra de flags /P

Solo ocho posiciones llevan significado, el resto está reservado, y el conteo empieza en 1. ISO 32000-1 §7.6.3.2 dice que P "debe interpretarse como una cantidad de 32 bits sin signo", numerada desde 1 en el extremo bajo hasta 32 en el alto, y pide a los lectores conformes que ignoren todo bit distinto de 3, 4, 5, 6, 9, 10, 11 y 12. ISO 32000-2 §7.6.4.2 corrige la Tabla 22 y detalla las posiciones reservadas: los bits 1–2 deben ser 0, los bits 7–8 deben ser 1 y, para la revisión 3 y posteriores, los bits 13–32 deben ser 1. PDF 2.0 además jubila el bit 10, que los lectores ya ignoran y los writers ponen siempre a 1 por los lectores viejos; la excepción de accesibilidad se muda al bit 5, así que un lector debe actuar como si copiar estuviera permitido cuando el contenido va a tecnología de asistencia. Una fórmula cubre cada fila de abajo: la máscara del bit n es 1 shl (n - 1) — y TPdfPermission se mapea sobre la palabra igual

BitMáscaraSignificado (revisión 3 y posteriores)TPdfPermission
1–2$00000003Reservado, debe ser 0ninguno
3$00000004Imprimir, con una calidad que depende del bit 12permPrint
4$00000008Modificar el contenido con operaciones distintas de las de los bits 6, 9 y 11permModify
5$00000010Copiar o extraer texto y gráficospermCopy
6$00000020Añadir o modificar anotaciones, rellenar campos; con el bit 4, crear campospermAnnotate
7–8$000000C0Reservado, debe ser 1ninguno
9$00000100Rellenar campos de formulario existentes, incluidos los de firma, aunque el bit 6 esté a ceropermFillForms
10$00000200Extracción para accesibilidad; sin uso en PDF 2.0, siempre escrito como 1permAccessibility
11$00000400Ensamblar: insertar, rotar o borrar páginas, crear items de esquema y miniaturaspermAssemble
12$00000800Imprimir en una representación fiel y de alta calidadpermPrintHighQuality
13–32$FFFFF000Reservado, debe ser 1 (ISO/TS 32004 asigna el bit 13, ver más abajo)ninguno

La máscara $FFFFFFF0 nace de leer "los bits bajos están reservados" y limpiar un nibble entero por si acaso. La región reservada eran solo los bits 1–2, máscara $3; limpiar $F también arrasa $4 y $8, que son print y modify, y cualquier viewer que honre los flags se negará a ambas. El error especular es construir la máscara hacia arriba desde cero: OR entre $4, $10 y $800 y obtienes un /P positivo pequeño con los bits 7–8 y 13–32 a cero. La mayoría de los lectores abre ese fichero sin comentario, porque la especificación les dice que ignoren los bits reservados, pero el valor es no conforme para la revisión 3 y posteriores y un verificador estricto tiene derecho a quejarse. Una tercera variante aparece en código que cuenta los bits desde cero, donde "bit 3" se convierte en $8 y el programa concede modify cuando quería print. Ninguna de estas produce un error en el guardado: los permisos simplemente salen distintos de los que el autor pretendía

Layout de la palabra de flags de permisos /P de PDF en PDFium Component para Delphi: bits reservados 1-2, 7-8 y 13-32, los bits de TPdfPermission como permPrint y permModify, y las máscaras $FFFFFFFC y $FFFFFFF0
Limpiar el nibble bajo entero con $FFFFFFF0 también elimina print y modify, mientras que construir la máscara desde cero deja a cero los reservados de la revisión 3 y posteriores

¿Es /P -4 el mismo valor que /P 4294967292?

Sí. Los mismos 32 bits, dos grafías. Los objetos integer de PDF llevan signo, y la nota justo encima de la Tabla 22 señala que, como los bits altos reservados son 1, el integer se escribe como número negativo; -4 es el valor convencional de todo permitido para la revisión 3 y posteriores. Los productores que guardan el valor en una variable sin signo escriben 4294967292, y ambas grafías aparecen en ficheros reales. La especificación decide qué lectura es la correcta en su propia derivación de clave: el paso (d) del Algoritmo 2 de ISO 32000-1 §7.6.3.3 convierte P en un número binario de 32 bits sin signo y pasa los cuatro bytes a MD5 con el byte bajo primero, así que para las revisiones 2 a 4 los valores -3904 y 4294963392 producen la misma clave de fichero. Un parser que guarda /P en un Integer de 32 bits con signo rechaza la grafía sin signo como desbordamiento; cambia la variable a Cardinal y la que falla es la negativa, mientras que un código que compara el texto del token contra un valor almacenado acaba tratando un documento como dos. Lee el token en un Int64, acepta cualquier cosa de Low(Integer) a High(Cardinal) y enmascara a 32 bits antes de cualquier comparación. El código de descifrado del propio PDFium Component lee /P así para las revisiones 2 a 7, y su writer emite la forma con signo que muestra la nota de la especificación. El informe de bits reservados del snippet se detiene en el bit 14, porque un bit 13 a cero es cómo marca ISO/TS 32004 un fichero que lleva un PDF MAC

uses
  System.SysUtils;

// Normaliza el texto del token /P de tu propio parser de diccionarios
function TryNormalizePermissions(const Token: string;
  out Mask: Cardinal): Boolean;
var
  Value: Int64;
begin
  Result := TryStrToInt64(Token, Value) and
    (Value >= Low(Integer)) and (Value <= High(Cardinal));
  if Result then
    Mask := Cardinal(Value and $FFFFFFFF);
end;

function DescribeReservedBits(Mask: Cardinal): string;
begin
  Result := '';
  if (Mask and $00000003) <> 0 then
    Result := Result + 'bits 1-2 set; ';
  if (Mask and $000000C0) <> $000000C0 then
    Result := Result + 'bits 7-8 clear; ';
  // El bit 13 ($1000) se queda fuera: ISO/TS 32004 lo pone a cero para exigir un PDF MAC
  if (Mask and $FFFFE000) <> $FFFFE000 then
    Result := Result + 'bits 14-32 not all set; ';
end;

// Esperado: ambas llamadas producen $FFFFFFFC y un informe de bits reservados vacío
//   TryNormalizePermissions('-4', M1);
//   TryNormalizePermissions('4294967292', M2);
Normalización del valor /P de PDF en Delphi como hace PDFium Component: /P -4 y /P 4294967292 se leen en Int64, se comprueba el rango de Low(Integer) a High(Cardinal) y se enmascaran al Cardinal $FFFFFFFC
Los parsers con Integer con signo y con Cardinal rechazan cada uno una grafía legal de /P, y leer en Int64 y luego enmascarar a 32 bits lleva ambas grafías a un solo valor

Construir una máscara desde un conjunto TPdfPermissions

Empieza con cada bit reservado en su valor exigido y haz OR solo con lo que concedas, así que un permiso olvidado solo puede convertirse en una denegación. TPdfEncryptOptions.Default de la unidad FPdfEncrypt ya trae $FFFFFFFC, que lo permite todo, y limpiarle uno o dos bits funciona bien para una restricción rápida. Una política que lista lo que los destinatarios pueden hacer se lee mejor como conjunto, y el array de constantes de abajo se alinea con las posiciones de bit que TPdf usa para decodificar la palabra. Pon el bit 10 a 1 sin importar lo que pase el llamador: ISO 32000-2 dice a los writers que lo pongan siempre, los lectores de PDF 2.0 lo ignoran de todos modos, y dejarlo a cero solo arriesga bloquear screen readers en software viejo

uses
  System.SysUtils, PDFium, FPdfEncrypt;

const
  PermissionBit: array[TPdfPermission] of Cardinal = (
    $0004,   // permPrint             bit 3
    $0008,   // permModify            bit 4
    $0010,   // permCopy              bit 5
    $0020,   // permAnnotate          bit 6
    $0100,   // permFillForms         bit 9
    $0200,   // permAccessibility     bit 10
    $0400,   // permAssemble          bit 11
    $0800);  // permPrintHighQuality  bit 12

  // Bits 7-8 y 13-32 a uno; bits 1-2 y todos los bits de permisos a cero
  ReservedOnes = Cardinal($FFFFF0C0);

function PermissionsToMask(Allowed: TPdfPermissions): Cardinal;
var
  P: TPdfPermission;
begin
  Result := ReservedOnes or PermissionBit[permAccessibility];
  for P in Allowed do
    Result := Result or PermissionBit[P];
end;

procedure SaveFillAndPrintCopy(const Source, Target: string;
  const OwnerPassword: AnsiString);
var
  Pdf: TPdf;
  Opts: TPdfEncryptOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := Source;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.CreateFmt('Could not open %s', [Source]);

    Opts := TPdfEncryptOptions.Default;   // aquí Revision = erR5
    Opts.UserPassword := '';              // cualquiera puede abrir el fichero
    Opts.OwnerPassword := OwnerPassword;  // protege el conjunto de permisos
    Opts.Revision := erR6;
    Opts.Permissions := PermissionsToMask(
      [permPrint, permPrintHighQuality, permFillForms]);

    // Esperado: /P -1084 = 4294966212 ($FFFFFBC4)
    Writeln('/P ', Integer(Opts.Permissions), ' = ', Opts.Permissions);

    if not Pdf.SaveAsEncrypted(Target, Opts) then
      raise Exception.Create('Encrypted save failed');
  finally
    Pdf.Free;
  end;
end;

Lo esperado: el diccionario Encrypt de la salida lleva /P -1084, y un viewer que honre los flags ofrece imprimir a máxima calidad y rellenar formularios mientras pone en gris la edición de contenido, la copia, los comentarios y el ensamblado de páginas. El bit 13 es una posición que el writer gestiona por ti. Con EnableIntegrityProtection dejado en False, SaveAsEncrypted pone el bit 13 a uno diga lo que diga tu máscara; con él a True, el writer limpia el bit 13 tanto en /P como en /Perms. Los demás bits reservados siguen siendo cosa tuya — de ahí el punto de partida $FFFFF0C0. El ejemplo elige erR6 porque R6 es la revisión AES-256 que estandariza ISO 32000-2; el registro por defecto trae erR5

/Perms, la copia cifrada de /P en los ficheros AES-256

En las revisiones 2 a 4 la palabra de permisos está soldada a la clave: cambia un bit de /P y la entrada de MD5 cambia, la clave derivada queda mal y no se descifra nada. Los ficheros AES-256 rompen ese vínculo. El Algoritmo 2.A de ISO 32000-2 §7.6.4.3.3 desenvuelve la clave de cifrado del fichero de /UE o /OE, y P no juega ningún papel en esa derivación, así que por sí solo un /P en claro podría editarse a placer. /Perms cierra la brecha. El Algoritmo 10 (§7.6.4.4.9) rellena un bloque de 16 bytes y lo cifra con la clave del fichero usando AES-256 en modo ECB:

  • bytes 0–3: la palabra de permisos, byte bajo primero, extendida a 64 bits por los bytes 4–7, que son todos $FF
  • byte 8: ASCII T o F, coincidiendo con /EncryptMetadata
  • bytes 9–11: el marcador ASCII adb
  • bytes 12–15: datos aleatorios que los lectores ignoran

El paso (f) del Algoritmo 2.A descifra el bloque, verifica adb y exige que los bytes 0–3 coincidan con P — shall, no should. El Algoritmo 13 (§7.6.4.4.12) repite las comprobaciones de marcador y permisos con el should más blando y añade una más: el byte 8 contra EncryptMetadata. PDFium Component lo lee de la forma estricta dondequiera que su propio código abre la capa de cifrado: descifrado AESV4, validación de PDF MAC y guardados incrementales en ficheros cifrados. Tras la contraseña de usuario o owner desbloquea la clave del fichero, ese código compara los cuatro bytes de permisos contra el /P normalizado, comprueba el relleno $FF, el byte T/F y el marcador, y hace fallar la carga con "PDF encryption permissions validation failed" ante cualquier diferencia, porque un desajuste significa que alguien editó el diccionario después del cifrado y ninguna de las dos copias puede tomarse al pie de la letra

Bloque /Perms AES-256 en PDFium Component: la palabra de permisos /P con el byte bajo primero, el relleno $FF, el byte T o F de /EncryptMetadata y el marcador adb, cifrado por el Algoritmo 10 y verificado bajo los Algoritmos 2.A y 13
Como la clave AES-256 del fichero ya no depende de /P, los bytes de /Perms descifrados son lo que demuestra que la palabra de permisos en claro no se editó después del cifrado

La comparación de EncryptMetadata merece cuidado en cualquier lector que escribas. Un atajo como Pos('/EncryptMetadata false', Dict) > 0 se pierde un /EncryptMetadata false con dos espacios, un salto de línea entre clave y valor o un comentario en medio, todo sintaxis PDF legal. El lector asume entonces el valor por defecto true, encuentra F en un /Perms perfectamente válido y rechaza el fichero. Un fichero válido, rechazado. Parsea la clave y su valor como tokens, y trata un valor no booleano como malformado en lugar de aplicarle el defecto

ISO/TS 32004 le da trabajo al bit 13: cuando está a cero, el documento lleva un PDF MAC que los lectores deben comprobar. El token del MAC vive en el diccionario /AuthCode del trailer, que no va cifrado, así que una comprobación que solo busque /AuthCode concluye que un fichero con él borrado nunca estuvo protegido contra manipulación. Borrar también /KDFSalt no esconde el requisito, porque el bit a cero vive en /P y /P está autenticado por /Perms. El descifrado AESV4 de PDFium Component trata por tanto un bit 13 a cero, o un /KDFSalt sobreviviente sin /AuthCode, como un MAC desvalijado y falla, y cuando la protección de integridad está apagada el writer pone el bit 13 a uno para que su salida nunca anuncie un MAC que no tiene. La jerarquía de claves y el byte range detrás de ese token pertenecen a el cifrado AES-GCM y el MAC de ISO/TS 32004 en PDF 2.0, y el hash de contraseña iterado que desbloquea la clave del fichero antes de que nada de esto corra está cubierto en el hash de contraseña R6 y su conteo de rondas

¿Permissions o UserPermissions: cuál debe leer tu interfaz?

Lee Permissions para decidir qué puede hacer la sesión actual y UserPermissions para informar de lo que el fichero concede a un destinatario corriente. TPdf.Permissions envuelve el FPDF_GetDocPermissions de PDFium y describe el acceso que se concedió a este documento abierto, que depende de la contraseña que lo desbloqueó. TPdf.UserPermissions envuelve FPDF_GetDocUserPermissions y sigue informando de la concesión de acceso de usuario de /P sin importar qué contraseña se usó, así que las dos pueden diferir cuando la contraseña de owner abrió el fichero. Ambas son LongWord; GrantedPermissions y UserGrantedPermissions las decodifican en conjuntos TPdfPermissions, y HasPermission y UserHasPermission prueban un miembro suelto. SecurityHandlerRevision te dice si el fichero tiene handler de seguridad. La rutina de abajo imprime ambas vistas enteras de la palabra en bruto junto al conjunto decodificado, una forma rápida de ver por qué un fichero "no imprime"

uses
  System.SysUtils, System.TypInfo, PDFium;

procedure DumpPermissions(const FileName, Password: string);
var
  Pdf: TPdf;
  Mask: LongWord;
  Granted: TPdfPermissions;
  P: TPdfPermission;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Password := Password;   // fíjalo antes de Active
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.CreateFmt('Could not open %s', [FileName]);

    if Pdf.SecurityHandlerRevision <= 0 then
    begin
      Writeln('Not encrypted: no /P to decode');
      Exit;
    end;

    Mask := Pdf.UserPermissions;
    Granted := Pdf.UserGrantedPermissions;
    Writeln(Format('R%d  /P as Integer %d, as Cardinal %u ($%.8x)',
      [Pdf.SecurityHandlerRevision, Integer(Mask), Mask, Mask]));

    for P := Low(TPdfPermission) to High(TPdfPermission) do
      Writeln(Format('  %-22s %s',
        [GetEnumName(TypeInfo(TPdfPermission), Ord(P)),
         BoolToStr(P in Granted, True)]));

    if not Pdf.HasPermission(permCopy) then
      Writeln('  this session may not copy text');
  finally
    Pdf.Free;
  end;
end;

// Esperado para el fichero escrito antes:
//   R6  /P as Integer -1084, as Cardinal 4294966212 ($FFFFFBC4)
//   permPrint True, permModify False, permCopy False, ...

La aplicación host se encarga de hacer cumplir

PDFium renderiza, extrae texto y rasteriza páginas digan lo que digan los flags. El motor reporta permisos y deja la política al llamador. Quien tiene la contraseña de usuario tiene el contenido descifrado — con una contraseña de usuario vacía, eso es todo el mundo — así que la confidencialidad solo puede venir de una contraseña de usuario que el lector no tenga. Mete la comprobación dentro de cada manejador de comando (copiar, exportar, borrar páginas, imprimir) en lugar de esconder botones de la barra, porque los atajos, los menús contextuales y la automatización se saltan una barra de herramientas, la misma estratificación usada en un panel de previsualización PDF segura para aplicaciones Delphi. Un comando de imprimir, por ejemplo, comprueba permPrint antes de mandar nada a la cola

En un fichero AES-256, un -1084 en texto plano no prueba nada hasta que los primeros cuatro bytes descifrados de /Perms lean C4 FB FF FF. Esa comparación byte a byte corre dentro del código AESV4, PDF MAC y de guardado incremental cifrado del PDFium Component