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
| Bit | Máscara | Significado (revisión 3 y posteriores) | TPdfPermission |
|---|---|---|---|
| 1–2 | $00000003 | Reservado, debe ser 0 | ninguno |
| 3 | $00000004 | Imprimir, con una calidad que depende del bit 12 | permPrint |
| 4 | $00000008 | Modificar el contenido con operaciones distintas de las de los bits 6, 9 y 11 | permModify |
| 5 | $00000010 | Copiar o extraer texto y gráficos | permCopy |
| 6 | $00000020 | Añadir o modificar anotaciones, rellenar campos; con el bit 4, crear campos | permAnnotate |
| 7–8 | $000000C0 | Reservado, debe ser 1 | ninguno |
| 9 | $00000100 | Rellenar campos de formulario existentes, incluidos los de firma, aunque el bit 6 esté a cero | permFillForms |
| 10 | $00000200 | Extracción para accesibilidad; sin uso en PDF 2.0, siempre escrito como 1 | permAccessibility |
| 11 | $00000400 | Ensamblar: insertar, rotar o borrar páginas, crear items de esquema y miniaturas | permAssemble |
| 12 | $00000800 | Imprimir en una representación fiel y de alta calidad | permPrintHighQuality |
| 13–32 | $FFFFF000 | Reservado, 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
¿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);
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
ToF, 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
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