Você criptografa um relatório com uma máscara que deveria permitir tudo, $FFFFFFF0, e a aba Security do Acrobat lista imprimir e alterar o documento como não permitido. O /P é uma palavra de flags de 32 bits sem sinal numerada a partir do bit 1, então essa máscara limpa print (bit 3) e modify (bit 4); o valor tudo-permitido é $FFFFFFFC, o ponto de partida do TPdfEncryptOptions no PDFium Component
Como a palavra de flags /P está disposta
Só oito posições carregam significado, o resto é reservado, e a contagem começa em 1. A ISO 32000-1 §7.6.3.2 diz que P "shall be interpreted as an unsigned 32-bit quantity", numerado de 1 na ponta de baixa ordem até 32 na de alta ordem, e manda readers conformes ignorar todo bit que não seja 3, 4, 5, 6, 9, 10, 11 e 12. A ISO 32000-2 §7.6.4.2 corrige a Tabela 22 e soletra as posições reservadas: bits 1–2 precisam ser 0, bits 7–8 precisam ser 1, e para revision 3 e posteriores bits 13–32 precisam ser 1. O PDF 2.0 também aposenta o bit 10, que readers agora ignoram e writers sempre definem como 1 para readers mais antigos; a exceção de acessibilidade vai para o bit 5, então um reader deve agir como se copiar fosse permitido quando o conteúdo vai para tecnologia assistiva. Uma fórmula cobre todas as linhas abaixo. A máscara do bit n é 1 shl (n - 1) — e o TPdfPermission mapeia na palavra do mesmo jeito
| Bit | Máscara | Significado (revision 3 e posteriores) | TPdfPermission |
|---|---|---|---|
| 1–2 | $00000003 | Reservado, precisa ser 0 | nenhum |
| 3 | $00000004 | Imprimir, com qualidade que depende do bit 12 | permPrint |
| 4 | $00000008 | Modificar conteúdo por operações outras que as dos bits 6, 9 e 11 | permModify |
| 5 | $00000010 | Copiar ou extrair texto e gráficos | permCopy |
| 6 | $00000020 | Acrescentar ou modificar annotations, preencher fields; com o bit 4, criar fields | permAnnotate |
| 7–8 | $000000C0 | Reservado, precisa ser 1 | nenhum |
| 9 | $00000100 | Preencher form fields existentes, incluindo signature fields, mesmo com o bit 6 limpo | permFillForms |
| 10 | $00000200 | Extração de acessibilidade; sem uso no PDF 2.0, sempre gravado como 1 | permAccessibility |
| 11 | $00000400 | Montar: inserir, rotacionar ou excluir páginas, criar itens de outline e thumbnails | permAssemble |
| 12 | $00000800 | Imprimir em uma representação fiel e de alta qualidade | permPrintHighQuality |
| 13–32 | $FFFFF000 | Reservado, precisa ser 1 (a ISO/TS 32004 atribui o bit 13, veja abaixo) | nenhum |
A máscara $FFFFFFF0 nasce de ler "os bits baixos são reservados" e limpar um nibble inteiro por segurança. A região reservada era só os bits 1–2, máscara $3; limpar $F também apaga $4 e $8, que são print e modify, e qualquer viewer que honra as flags recusa os dois. O erro espelho é montar a máscara para cima a partir de zero: faça OR de $4, $10 e $800 e você recebe um /P positivo pequeno com bits 7–8 e 13–32 limpos. A maioria dos readers abre esse arquivo sem comentário, porque a especificação manda ignorar bits reservados, mas o valor é não conforme para revision 3 e posteriores e um checker estrito tem o direito de reclamar. Uma terceira variante aparece em código que conta bits a partir de zero, onde "bit 3" vira $8 e o programa concede modify quando queria print. Nenhuma delas produz erro na hora de salvar. As permissões simplesmente saem diferentes das que o autor pretendia
O /P -4 é o mesmo valor que /P 4294967292?
Sim. Os mesmos 32 bits, duas grafias. Objetos integer PDF têm sinal, e a nota logo acima da Tabela 22 aponta que, como os bits de alta ordem reservados são 1, o integer é gravado como número negativo; -4 é o valor tudo-permitido convencional para revision 3 e posteriores. Produtores que mantêm o valor em uma variável sem sinal gravam 4294967292, e as duas grafias aparecem em arquivos reais. A especificação resolve qual leitura é a certa na própria derivação de chave dela: o passo (d) do Algorithm 2 na ISO 32000-1 §7.6.3.3 converte P em um número binário de 32 bits sem sinal e passa os quatro bytes ao MD5 com o byte de baixa ordem primeiro, então para revisions 2 a 4 os valores -3904 e 4294963392 produzem a mesma file key. Um parser que guarda /P em um Integer de 32 bits com sinal rejeita a grafia sem sinal como overflow; troque a variável para Cardinal e a grafia negativa falha em vez disso, enquanto código que compara o texto do token contra um valor armazenado acaba tratando um documento como dois. Leia o token em um Int64, aceite qualquer coisa de Low(Integer) a High(Cardinal), e aplique máscara de 32 bits antes de qualquer comparação. O código de decrypt do próprio PDFium Component lê o /P assim para revisions 2 a 7, e o writer dele emite a forma com sinal que a nota da especificação mostra. O relatório de bits reservados no snippet para no bit 14, já que um bit 13 limpo é como a ISO/TS 32004 marca um arquivo que carrega um PDF MAC
uses
System.SysUtils;
// Normalize o texto do token /P vindo do seu parser de dicionário
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; ';
// Bit 13 ($1000) fica de fora: a ISO/TS 32004 o limpa para exigir um PDF MAC
if (Mask and $FFFFE000) <> $FFFFE000 then
Result := Result + 'bits 14-32 not all set; ';
end;
// Esperado: ambas as chamadas rendem $FFFFFFFC e um relatório de bits reservados vazio
// TryNormalizePermissions('-4', M1);
// TryNormalizePermissions('4294967292', M2);
Montando uma máscara a partir de um set TPdfPermissions
Comece com todo bit reservado no valor exigido dele e faça OR apenas do que você concede, para que uma permissão esquecida só possa virar uma negação. O TPdfEncryptOptions.Default na unit FPdfEncrypt já guarda $FFFFFFFC, que permite tudo, e limpar um ou dois bits dele funciona bem para uma restrição rápida. Uma política que lista o que os destinatários podem fazer lê melhor como um set, e o array constante abaixo se alinha com as posições de bit que o TPdf usa para decodificar a palavra. Force o bit 10 para 1 sem importar o que o chamador passar. A ISO 32000-2 manda writers sempre defini-lo, readers PDF 2.0 o ignoram de qualquer forma, e deixá-lo limpo só arrisca bloquear screen readers em software mais antigo
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 e 13-32 definidos; bits 1-2 e todo bit de permissão limpos
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; // aqui Revision = erR5
Opts.UserPassword := ''; // qualquer um pode abrir o arquivo
Opts.OwnerPassword := OwnerPassword; // protege o conjunto de permissões
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;
Esperado: o dicionário Encrypt da saída carrega /P -1084, e um viewer que honra as flags oferece impressão em qualidade plena e preenchimento de formulários enquanto acincenta edições de conteúdo, cópia, comentários e montagem de páginas. O bit 13 é uma posição que o writer cuida para você. Com EnableIntegrityProtection deixado em False, o SaveAsEncrypted define o bit 13 não importe o que a sua máscara diga; com ele em True, o writer limpa o bit 13 tanto no /P quanto no /Perms. Os outros bits reservados seguem sendo responsabilidade sua — daí o ponto de partida $FFFFF0C0. O exemplo escolhe erR6 porque R6 é a revision AES-256 que a ISO 32000-2 padroniza; o record default carrega erR5
/Perms, a cópia criptografada do /P em arquivos AES-256
Nas revisions 2 a 4 a palavra de permissão está soldada à chave: mude um bit do /P e a entrada do MD5 muda, a chave derivada fica errada, e nada descriptografa. Arquivos AES-256 quebram esse vínculo. O Algorithm 2.A na ISO 32000-2 §7.6.4.3.3 desembrulha a file encryption key do /UE ou do /OE, e o P não participa dessa derivação, então por si só um /P em texto puro poderia ser editado à vontade. O /Perms fecha essa lacuna. O Algorithm 10 (§7.6.4.4.9) preenche um bloco de 16 bytes e o criptografa com a file key usando AES-256 em modo ECB:
- bytes 0–3: a palavra de permissão, byte de baixa ordem primeiro, estendida a 64 bits pelos bytes 4–7, que são todos
$FF - byte 8: ASCII
TouF, casando com o/EncryptMetadata - bytes 9–11: o marcador ASCII
adb - bytes 12–15: dados aleatórios que readers ignoram
O passo (f) do Algorithm 2.A descriptografa o bloco, verifica o adb e exige que os bytes 0–3 batam com o P — shall, não should. O Algorithm 13 (§7.6.4.4.12) repete as checagens de marcador e permissão com o should mais frouxo e acrescenta mais uma: o byte 8 contra o EncryptMetadata. O PDFium Component lê do jeito estrito onde o próprio código dele abre a camada de criptografia: decrypt AESV4, validação de PDF MAC e saves incrementais em arquivos criptografados. Depois que a user password ou a owner password destrava a file key, esse código compara os quatro bytes de permissão contra o /P normalizado, confere o preenchimento $FF, o byte T/F e o marcador, e reprova o load com "PDF encryption permissions validation failed" em qualquer diferença, porque uma divergência significa que alguém editou o dicionário depois da criptografia e nenhuma das cópias pode ser tomada ao pé da letra
A comparação do EncryptMetadata merece cuidado em qualquer reader que você escrever. Um atalho como Pos('/EncryptMetadata false', Dict) > 0 não vê /EncryptMetadata false com dois espaços, uma quebra de linha entre chave e valor, ou um comentário no meio, tudo sintaxe PDF legal. O reader então assume o default true, acha F num /Perms perfeitamente válido, e recusa o arquivo. Um arquivo válido, recusado. Faça o parse da chave e do valor dela como tokens, e trate um valor não boolean como malformado em vez de aplicar default
A ISO/TS 32004 dá ao bit 13 um trabalho: quando está limpo, o documento carrega um PDF MAC que readers precisam checar. O token do MAC mora no dicionário /AuthCode do trailer, que não é criptografado, então uma checagem que só procura /AuthCode conclui que um arquivo com ele apagado jamais teve proteção de integridade. Apagar o /KDFSalt também não esconde a exigência, porque o bit limpo mora no /P e o /P é autenticado pelo /Perms. O decrypt AESV4 do PDFium Component portanto trata um bit 13 limpo, ou um /KDFSalt sobrando, sem um /AuthCode como MAC arrancado e reprova, e quando a proteção de integridade está desligada o writer define o bit 13 para que a saída dele nunca anuncie um MAC que não tem. A hierarquia de chaves e o byte range por trás desse token pertencem a criptografia AES-GCM e o MAC ISO/TS 32004 no PDF 2.0, e o hash de senha iterado que destroca a file key antes de qualquer disso rodar está coberto em o hash de senha R6 e a contagem de rounds dele
Permissions ou UserPermissions: qual a sua UI deve ler?
Leia Permissions para decidir o que a sessão corrente pode fazer, e UserPermissions para reportar o que o arquivo concede a um destinatário comum. O TPdf.Permissions embrulha o FPDF_GetDocPermissions do PDFium e descreve o acesso concedido a este documento aberto, o que depende da senha que o destravou. O TPdf.UserPermissions embrulha o FPDF_GetDocUserPermissions e continua reportando a concessão de acesso de usuário do /P não importe qual senha tenha sido usada, então os dois podem divergir quando a owner password abriu o arquivo. Ambos são LongWord; GrantedPermissions e UserGrantedPermissions os decodificam em sets TPdfPermissions, e HasPermission e UserHasPermission testam um único membro. O SecurityHandlerRevision diz se o arquivo tem security handler algum. A rotina abaixo imprime as duas vistas inteiras da palavra crua ao lado do set decodificado, um jeito rápido de ver por que um arquivo "não 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; // defina 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 o arquivo gravado antes:
// R6 /P as Integer -1084, as Cardinal 4294966212 ($FFFFFBC4)
// permPrint True, permModify False, permCopy False, ...
A execução cabe à aplicação host
O PDFium renderiza, extrai texto e rasteriza páginas não importe o que as flags digam. O motor reporta permissões e deixa a política para o chamador. Quem segura a user password segura o conteúdo descriptografado — com user password vazia, isso é todo mundo — então confidencialidade só pode vir de uma user password que o reader não tem. Ponha a checagem dentro de cada command handler (copiar, exportar, excluir página, imprimir) em vez de esconder botões de toolbar, já que shortcuts, menus de contexto e automação todos passam por cima de uma toolbar, o mesmo layering usado em um painel de preview seguro de PDF para aplicações Delphi. Um command de impressão, por exemplo, testa permPrint antes de mandar qualquer coisa para o spool
Num arquivo AES-256, -1084 em texto puro não prova nada até que os primeiros quatro bytes descriptografados do /Perms leiam C4 FB FF FF. Essa comparação byte a byte roda dentro do código AESV4, PDF MAC e save incremental criptografado do PDFium Component