Artigo Técnico

Flags de permissão PDF, o inteiro /P e a cópia /Perms

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

BitMáscaraSignificado (revision 3 e posteriores)TPdfPermission
1–2$00000003Reservado, precisa ser 0nenhum
3$00000004Imprimir, com qualidade que depende do bit 12permPrint
4$00000008Modificar conteúdo por operações outras que as dos bits 6, 9 e 11permModify
5$00000010Copiar ou extrair texto e gráficospermCopy
6$00000020Acrescentar ou modificar annotations, preencher fields; com o bit 4, criar fieldspermAnnotate
7–8$000000C0Reservado, precisa ser 1nenhum
9$00000100Preencher form fields existentes, incluindo signature fields, mesmo com o bit 6 limpopermFillForms
10$00000200Extração de acessibilidade; sem uso no PDF 2.0, sempre gravado como 1permAccessibility
11$00000400Montar: inserir, rotacionar ou excluir páginas, criar itens de outline e thumbnailspermAssemble
12$00000800Imprimir em uma representação fiel e de alta qualidadepermPrintHighQuality
13–32$FFFFF000Reservado, 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

Layout da palavra de flags de permissão /P do PDF no Delphi com PDFium Component: bits reservados 1-2, 7-8 e 13-32, os bits TPdfPermission como permPrint e permModify, e as máscaras $FFFFFFFC e $FFFFFFF0
limpar o nibble baixo inteiro com $FFFFFFF0 também remove print e modify, enquanto montar a máscara de baixo para cima a partir de zero deixa os reservados de revision 3 e posteriores limpos

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);
Normalizando o valor /P do PDF no Delphi como o PDFium Component faz: /P -4 e /P 4294967292 lidos em Int64, conferidos na faixa de Low(Integer) a High(Cardinal) e mascarados para o Cardinal $FFFFFFFC
parsers Integer com sinal e Cardinal rejeitam cada um uma grafia legal do /P, e ler em Int64 e depois mascarar para 32 bits mapeia as duas grafias para um único valor

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 T ou F, 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

Bloco /Perms AES-256 no PDFium Component: a palavra de permissão /P com byte de baixa ordem primeiro, preenchimento $FF, o byte T ou F do /EncryptMetadata e o marcador adb, criptografado pelo Algorithm 10 e verificado sob os Algorithms 2.A e 13
como a file key AES-256 não depende mais do /P, os bytes /Perms descriptografados são o que prova que a palavra de permissão em texto puro não foi editada após a criptografia

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