Pular para o conteúdo

Criptografar um PDF e restringir permissões

Esta receita criptografa um documento com o handler de segurança padrão do Advanced Encryption Standard (AES)-256. Você define uma senha de usuário (necessária para abrir), uma senha de proprietário (acesso total) e uma bitmask de permissões que restringe operações. Trate essas permissões como dependentes da cooperação do leitor: a criptografia fornece confidencialidade, não integridade, e apenas softwares cooperativos respeitam os bits de permissão. A receita segue examples/22-protection.php.

Fronteira de confiança (considere isto em toda afirmação sobre permissões). A criptografia de PDF protege a confidencialidade do conteúdo contra partes sem a senha (ISO 32000-2 §7.6). Ela não protege a integridade: não detecta nem impede modificação. A entrada de permissão P é um conjunto de flags de 32 bits sem sinal que solicita aos leitores compatíveis que respeitem as restrições; ela não é um controle de acesso. Uma ferramenta não compatível, ou qualquer ferramenta usada com a senha de proprietário, pode realizar todas as operações “negadas”. Não descreva um PDF criptografado como “seguro”, “à prova de adulteração” ou “protegido contra cópia”.

Terminal window
composer require nextpdf/core:^3

Habilite a extensão PHP openssl. O criptografador AES-256 a usa para a cifra e a derivação de chave.

Os códigos V/R do dicionário de criptografia selecionam o handler de segurança padrão (ISO 32000-2 §7.6). O Aes256Encryptor do NextPDF implementa o crypt filter AESV3 na revisão 6 do handler de segurança (V=5/R=6). Ele usa uma chave aleatória de criptografia de arquivo de 256 bits, derivação de chave por hash iterativo com salt (Algoritmo 2.B) e criptografia AES-256-CBC por objeto com um vetor de inicialização aleatório. O Cipher Block Chaining (CBC) é um modo de confidencialidade (NIST SP 800-38A). Seus vetores de inicialização devem ser imprevisíveis.

O vetor de inicialização é renovado para cada objeto e a cada execução; por isso, os bytes brutos diferem de uma execução para outra. O perfil de reprodutibilidade é, portanto, structural. Antes de comparar duas execuções, o harness canonicaliza o IV de criptografia, a ordem dos objetos e o /ID do trailer. Esse perfil é mais estrito do que o perfil de uma receita que omite a criptografia.

A bitmask de permissões define a entrada P. O bit 3 concede impressão, e o bit 6 concede annotation/form-fill. O valor é a quantidade documentada de 32 bits sem sinal.

NextPDF\Core\Concerns\HasSecurity (incluído em Document):

  • setEncryption(#[SensitiveParameter] string $userPassword, #[SensitiveParameter] string $ownerPassword = '', int $permissions = -1): static — configura a criptografia AES-256 do handler padrão. permissions = -1 concede todas as permissões. Quando ownerPassword está vazia, a senha de usuário é reutilizada como senha de proprietário. Chame antes de addPage().
  • getEncryptor(): ?Aes256Encryptor — retorna o criptografador configurado, ou null.
  • useAesGcm(?bool $enabled = true): static — opta pelo AES-256-GCM da ISO/TS 32003; lança uma exceção se o OpenSSL/libsodium do host não disponibilizar a cifra.

Ambos os parâmetros de senha são marcados com #[SensitiveParameter], então o PHP os oculta dos stack traces.

Bits de permissão (a entrada P, bits baixos 3–6 de uso comum):

BitValorOperação
34Imprimir o documento
48Modificar o conteúdo do documento
516Copiar / extrair texto e gráficos
632Adicionar ou modificar anotações e preencher campos de formulário
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('Confidential Memo');
// Grant printing only (bit 3 = 4). MUST run before addPage().
$doc->setEncryption(
userPassword: 'open-me',
ownerPassword: 'owner-secret',
permissions: 4,
);
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'Encrypted with AES-256; printing allowed only.', newLine: true);
$doc->save(__DIR__ . '/confidential.pdf');
echo "Wrote confidential.pdf\n";

O exemplo completo abaixo espelha examples/22-protection.php e grava em NEXTPDF_COOKBOOK_OUTPUT para o harness.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$userPassword = 'demo';
$ownerPassword = 'admin';
// Grant ONLY printing (bit 3 = 4); deny copy/modify/annotate.
$permissions = 4;
$doc = Document::createStandalone();
$doc->setTitle('Encrypted Document — Restricted Permissions');
$doc->setAuthor('NextPDF Example');
// setEncryption() MUST be called before addPage().
$doc->setEncryption(
userPassword: $userPassword,
ownerPassword: $ownerPassword,
permissions: $permissions,
);
$doc->addPage();
$doc->setFont('helvetica', 'B', 20);
$doc->cell(0, 14, 'Encrypted PDF Document', newLine: true);
$doc->ln(8);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, 'This document is protected with AES-256 encryption '
. '(standard security handler, revision 6). The user password is required '
. 'to open it; the owner password grants full access. The permission '
. 'bits below are honoured by conforming readers only.');
$doc->ln(5);
$permissionTable = [
['Bit 3 (4)', 'Printing', 'ALLOWED'],
['Bit 4 (8)', 'Content modification', 'DENIED'],
['Bit 5 (16)', 'Text copying / extraction', 'DENIED'],
['Bit 6 (32)', 'Annotations / form fields', 'DENIED'],
];
$doc->setFont('helvetica', 'B', 10);
$doc->cell(30, 7, 'Flag');
$doc->cell(60, 7, 'Operation');
$doc->cell(0, 7, 'Status', newLine: true);
foreach ($permissionTable as [$bit, $operation, $status]) {
$doc->setFont('courier', '', 9);
$doc->cell(30, 7, $bit);
$doc->setFont('helvetica', '', 10);
$doc->cell(60, 7, $operation);
$doc->setFont('helvetica', 'B', 10);
$doc->cell(0, 7, $status, newLine: true);
}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$doc->save($out !== false ? $out : __DIR__ . '/encrypted.pdf');
echo "Wrote encrypted PDF (AES-256, printing only)\n";

Saída esperada:

Wrote encrypted PDF (AES-256, printing only)

Quando você abre o arquivo, o leitor solicita uma senha. A senha de usuário abre o arquivo com o conjunto restrito de permissões. A senha de proprietário abre o arquivo com acesso total.

  • Ordem de chamada. setEncryption() depois de addPage() não criptografa retroativamente o conteúdo anterior. Configure a criptografia primeiro; o motor criptografa o corpo de cada objeto conforme ele é escrito.
  • Padrão da senha de proprietário. Uma senha de proprietário vazia faz o motor reutilizar a senha de usuário como senha de proprietário. Na prática, isso elimina qualquer papel privilegiado. Defina senhas distintas quando os dois papéis precisarem ser diferentes.
  • A semântica das permissões é consultiva. Apenas leitores compatíveis respeitam os bits. Eles não são impostos criptograficamente: uma ferramenta não compatível, ou qualquer ferramenta usada com a senha de proprietário, pode realizar operações restritas. Trate as permissões como um sinal de política para softwares cooperativos, nunca como um controle de acesso capaz de resistir a uma parte determinada.
  • Nenhuma garantia de integridade. A criptografia oferece confidencialidade, não integridade. Um atacante sem a senha não consegue ler o conteúdo, mas o próprio formato não detecta adulteração. A proteção de integridade requer um mecanismo separado, como uma assinatura digital ou o document MAC da ISO/TS 32004.
  • Conflito com PDF/A. O PDF/A proíbe a chave de trailer Encrypt. Chamar setEncryption() em um documento PDF/A, em qualquer ordem, lança uma exceção de incompatibilidade.
  • Opt-in do AES-256-GCM. useAesGcm() seleciona a criptografia em massa GCM da ISO/TS 32003 quando o OpenSSL ou o libsodium do host a fornece. Caso contrário, lança InvalidConfigException. Ela é incompatível com o PDF/A pelo mesmo motivo.
  • A criptografia de chave pública ainda não está conectada. setPublicKeyEncryption() congela a superfície da API, mas save() lança uma exceção até que a conexão do writer seja entregue (um defeito conhecido). Não a use em produção no Core.

A derivação de chave executa o hash iterado do Algoritmo 2.B uma vez por documento. O AES-256-CBC por objeto é linear em relação ao tamanho do corpo do objeto. Para documentos típicos, o custo se mantém bem dentro do orçamento de 1500 ms / 64 MB. Documentos muito grandes incorrem em um custo de throughput de AES por objeto. O Galois/Counter Mode (GCM) com AES-NI é mais rápido em hosts compatíveis.

  • Apenas confidencialidade. Reiterando a fronteira de confiança: a criptografia mantém o conteúdo inacessível para partes sem a senha. Ela não prova que o arquivo está inalterado, e os bits de permissão dependem da cooperação do leitor.
  • A força da senha é sua responsabilidade. O handler é tão forte quanto as senhas. Depois que alguém obtém o arquivo, uma senha de usuário fraca pode ser quebrada por força bruta offline; o formato não consegue limitar a taxa de tentativas.
  • A senha de proprietário é uma chave primária. Qualquer pessoa com a senha de proprietário contorna todas as restrições. Trate-a como uma credencial root; nunca a entregue junto com o documento nem a registre em log.
  • #[SensitiveParameter] é defesa em profundidade. Ele oculta as senhas dos stack traces do PHP, mas você ainda precisa mantê-las fora dos seus próprios logs, mensagens de exceção e relatórios de falha.

A biblioteca realiza a criptografia em processo. Ela não transmite o documento nem as senhas para lugar algum. O motor não grava nenhuma senha, chave ou byte do documento em disco, exceto a saída criptografada que você salva. Onde o arquivo de saída reside, e como as senhas são guardadas, são preocupações de implantação que cabem ao integrador. A biblioteca não oferece nenhuma garantia de residência. Se o documento em texto claro contiver dados pessoais, esses dados estarão protegidos apenas até o limite imposto pela senha mais fraca e pela ressalva acima sobre leitores cooperativos. A criptografia não substitui a minimização das informações de identificação pessoal (PII) que você coloca no documento.

A criptografia emite um EncryptionAppliedEvent que carrega apenas o nome do algoritmo (AES-256) e três booleanos que resumem se print/copy/modify são permitidos — nenhuma senha, chave, salt ou IV é colocada no evento em nenhum momento (src/Event/Security/EncryptionAppliedEvent.php). O caminho do OpenTelemetry roteia os atributos de span por um sanitizador de allowlist (src/Telemetry/AttributeSanitizer.php) que rejeita incondicionalmente senhas e caminhos de arquivo; apenas chaves da allowlist com valores escalares sobrevivem. Não adicione material de senha ou chave a spans, logs ou mensagens de exceção no seu próprio código de integração. Os marcadores #[SensitiveParameter] protegem os stack traces, mas não as strings que você mesmo monta.

Dentro do escopo: um adversário que obtém o arquivo criptografado, mas não as senhas. Ele não consegue ler o conteúdo, sujeito à força da senha, e o arquivo não vaza texto claro. Fora do escopo: um adversário que tenha a senha de usuário ou de proprietário; um leitor não compatível que ignore os bits de permissão; força bruta offline de uma senha fraca; detecção de adulteração (a criptografia fornece confidencialidade, não integridade); canais laterais na build do OpenSSL do host; e a custódia de chaves, que é inteiramente responsabilidade do integrador. Documentar essas ameaças não afirma a ausência de vulnerabilidades.

A build do OpenSSL do host fornece as primitivas criptográficas, então a postura FIPS é uma propriedade do host, não uma configuração da biblioteca. CryptoCapabilities::detectFipsMode() retorna um FipsModeDetection de três estados (src/Security/FipsModeDetection.php): FIPS_ACTIVE, FIPS_ABSENT, ou INDETERMINATE. A extensão openssl do PHP não expõe nenhum binding para o modelo de provider do OpenSSL 3, então a sondagem é feita em regime de melhor esforço. INDETERMINATE é tratado como “FIPS não comprovado” (fail-closed), distinguível em telemetria acionável pelo operador. O NextPDF não reivindica validação FIPS 140; executar em um OpenSSL validado por FIPS é responsabilidade do operador, e o resultado da detecção é consultivo.

DeclaraçãoSpecCláusulareference_id
O código V do dicionário de criptografia seleciona o algoritmo de criptografia.ISO 32000-2§7.6
O método de crypt filter AESV3 é nomeado pela entrada CFM.ISO 32000-2§7.6
A entrada P é uma quantidade de permissão de acesso de 32 bits sem sinal.ISO 32000-2§7.6
O bit de permissão 3 controla a impressão.ISO 32000-2§7.6
O bit de permissão 6 controla anotação / preenchimento de formulário.ISO 32000-2§7.6
A criptografia protege o conteúdo contra acesso não autorizado (confidencialidade).ISO 32000-2§7.6
A derivação de chave da revisão 6 usa hashing iterativo com salt (Algoritmo 2.B).ISO 32000-2§7.6
O CBC é um modo de confidencialidade (não um modo de integridade).NIST SP 800-38A§6.2
Os vetores de inicialização do CBC devem ser imprevisíveis.NIST SP 800-38AAp. C

O NextPDF implementa as cláusulas citadas. Ele não afirma conformidade abrangente com a ISO 32000-2, validação FIPS 140, nem qualquer garantia legal ou contratual de confidencialidade. “Suporte ao handler de segurança padrão” não é uma certificação de segurança na sua implantação. Isso depende da custódia de senhas e da política do verificador, fora do escopo da biblioteca.