Salta ai contenuti

Cifrare un PDF e limitare le autorizzazioni

Questa ricetta mostra come cifrare un documento con il gestore di sicurezza standard AES-256. Configura una password utente (necessaria per l’apertura) e una password proprietario (accesso completo) e limita le operazioni tramite una bitmask di autorizzazioni. Rende esplicita la natura dipendente dalla cooperazione del lettore di tali autorizzazioni: la cifratura garantisce la riservatezza, non l’integrità, e i bit di autorizzazione sono rispettati soltanto dal software cooperante. La ricetta segue examples/22-protection.php.

Limite di fiducia (da tenere presente in ogni affermazione sulle autorizzazioni). La cifratura PDF protegge la riservatezza del contenuto nei confronti di chi non dispone della password (ISO 32000-2 §7.6). Non protegge l’integrità: non rileva né impedisce le modifiche. La voce di autorizzazione P è un insieme di flag a 32 bit senza segno che i lettori conformi dovrebbero rispettare; questi flag non costituiscono un controllo degli accessi. Uno strumento non conforme, o qualsiasi strumento usato con la password proprietario, può eseguire ogni operazione “negata”. Non descrivere un PDF cifrato come “sicuro”, “a prova di manomissione” o “protetto dalla copia”.

Terminal window
composer require nextpdf/core:^3

Abilitare l’estensione PHP openssl. Il componente di cifratura AES-256 la usa per il cifrario e la derivazione della chiave.

Il gestore di sicurezza standard si seleziona tramite i codici V/R del dizionario di cifratura (ISO 32000-2 §7.6). L’Aes256Encryptor di NextPDF implementa il crypt filter AESV3 nella revisione 6 del gestore di sicurezza (V=5/R=6): una chiave di cifratura del file casuale a 256 bit, una derivazione della chiave basata su hash iterativo e salt (Algoritmo 2.B) e la cifratura AES-256-CBC per oggetto con un vettore di inizializzazione casuale. CBC è una modalità di riservatezza (NIST SP 800-38A). I suoi IV devono essere imprevedibili.

L’IV viene generato di nuovo per ogni oggetto e per ogni esecuzione, quindi i byte grezzi differiscono da un’esecuzione all’altra. Il profilo di riproducibilità è pertanto structural. Per confrontare due esecuzioni, l’harness canonicalizza l’IV di cifratura, l’ordine degli oggetti e il /ID del trailer. Questo profilo è più rigoroso di quello di una ricetta che omette la cifratura.

La bitmask di autorizzazioni imposta la voce P. Il bit 3 concede la stampa, il bit 6 concede annotazione/compilazione dei moduli; il valore è la quantità a 32 bit senza segno documentata.

NextPDF\Core\Concerns\HasSecurity (integrato in Document):

  • setEncryption(#[SensitiveParameter] string $userPassword, #[SensitiveParameter] string $ownerPassword = '', int $permissions = -1): static — configura la cifratura AES-256 del gestore di sicurezza standard. permissions = -1 concede tutte le autorizzazioni. Quando ownerPassword è vuota, la password utente viene riutilizzata come password proprietario. Chiamare prima di addPage().
  • getEncryptor(): ?Aes256Encryptor — l’encryptor configurato, oppure null.
  • useAesGcm(?bool $enabled = true): static — abilita facoltativamente AES-256-GCM di ISO/TS 32003; genera un’eccezione se l’OpenSSL/libsodium dell’host non dispone del cifrario.

Entrambi i parametri delle password sono contrassegnati con #[SensitiveParameter], in modo che PHP li oscuri negli stack trace.

Bit di autorizzazione (la voce P, bit bassi 3–6 comunemente usati):

BitValoreOperazione
34Stampare il documento
48Modificare il contenuto del documento
516Copiare / estrarre testo e grafica
632Aggiungere o modificare annotazioni e compilare campi modulo
<?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";

L’esempio completo riportato di seguito rispecchia examples/22-protection.php e scrive in NEXTPDF_COOKBOOK_OUTPUT per l’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";

Output previsto:

Wrote encrypted PDF (AES-256, printing only)

L’apertura del file richiede una password. La password utente apre il documento con il set di autorizzazioni limitato. La password proprietario lo apre con accesso completo.

  • Ordine delle chiamate. setEncryption() dopo addPage() non cifra retroattivamente il contenuto precedente. Configurare sempre la cifratura prima; il motore cifra il corpo di ciascun oggetto man mano che viene scritto.
  • Valore predefinito della password proprietario. Se la password proprietario è vuota, il motore riutilizza la password utente come password proprietario; in tal caso non esiste di fatto alcun ruolo privilegiato. Impostare password distinte quando i due ruoli devono differire.
  • La semantica delle autorizzazioni è indicativa. I bit vengono rispettati soltanto dai lettori conformi. Non sono imposti crittograficamente: uno strumento non conforme, o qualsiasi strumento usato con la password proprietario, può eseguire le operazioni limitate. Considerare le autorizzazioni come un segnale di policy rivolto al software cooperante, mai come un controllo degli accessi in grado di resistere a un soggetto determinato.
  • Nessuna garanzia di integrità. La cifratura offre riservatezza, non integrità. Un attore malintenzionato privo della password non può leggere il contenuto, ma il formato di per sé non rileva eventuali manomissioni. Per la protezione dell’integrità è necessario un meccanismo separato (una firma digitale oppure un document MAC di ISO/TS 32004).
  • Conflitto con PDF/A. PDF/A vieta la chiave del trailer Encrypt. Chiamare setEncryption() su un documento PDF/A, in qualsiasi ordine, genera un’eccezione di incompatibilità.
  • Abilitazione di AES-256-GCM. useAesGcm() seleziona la cifratura di massa GCM di ISO/TS 32003 quando OpenSSL o libsodium dell’host la offrono; in caso contrario genera InvalidConfigException. È incompatibile con PDF/A per la stessa ragione.
  • La cifratura a chiave pubblica non è ancora collegata. setPublicKeyEncryption() fissa la superficie API ma save() genera un’eccezione finché il collegamento del writer non viene completato (difetto noto); non usarla in produzione su Core.

La derivazione della chiave esegue l’hash iterato dell’Algoritmo 2.B una volta per documento. L’AES-256-CBC per oggetto è lineare rispetto alla dimensione del corpo dell’oggetto. Per i documenti tipici il costo rimane ampiamente entro il budget di 1500 ms / 64 MB. I documenti molto grandi comportano un costo in termini di throughput AES per oggetto. GCM con AES-NI è più rapido sugli host che ne dispongono.

  • Solo riservatezza. Per ribadire il limite di fiducia: la cifratura tiene il contenuto al riparo da chi non dispone della password; non dimostra che il file sia inalterato e i bit di autorizzazione dipendono dalla cooperazione del lettore.
  • La robustezza della password dipende da te. La robustezza del gestore dipende dalla robustezza delle password. Una password utente debole è attaccabile con forza bruta offline una volta ottenuto il file; il formato non può limitare la frequenza dei tentativi.
  • La password proprietario è una chiave primaria. Chiunque disponga della password proprietario aggira ogni restrizione. Considerarla come una credenziale root; non distribuirla mai con il documento né registrarla nei log.
  • #[SensitiveParameter] è una difesa in profondità. Oscura le password dalle stack trace di PHP, ma è comunque necessario tenerle fuori dai propri log, dai messaggi di eccezione e dai report di crash.

Residenza dei dati e mitigazioni dei dati personali

Sezione intitolata “Residenza dei dati e mitigazioni dei dati personali”

La libreria esegue la cifratura in-process. Non trasmette il documento né le password ad alcun destinatario. Il motore non scrive su disco password, chiavi o byte del documento, fatta eccezione per l’output cifrato che si salva. La posizione del file di output e la custodia delle password rientrano negli aspetti di distribuzione di competenza dell’integratore. La libreria non fornisce alcuna garanzia di residenza. Se il documento in chiaro contiene dati personali, tali dati sono protetti solo nei limiti imposti dalla password più debole e dall’avvertenza sulla cooperazione del lettore riportata sopra. La cifratura non sostituisce la minimizzazione dei dati personali inseriti nel documento.

La cifratura emette un EncryptionAppliedEvent che riporta soltanto il nome dell’algoritmo (AES-256) e tre valori booleani che indicano se print/copy/modify sono consentiti — nessuna password, chiave, salt o IV viene mai inserita nell’evento (src/Event/Security/EncryptionAppliedEvent.php). Il percorso OpenTelemetry instrada gli attributi degli span attraverso un sanitizer basato su allowlist (src/Telemetry/AttributeSanitizer.php) che rifiuta incondizionatamente password e percorsi di file; vengono conservate soltanto le chiavi in allowlist con valori scalari. Non aggiungere materiale relativo a password o chiavi a span, log o messaggi di eccezione nel proprio codice di integrazione — i marcatori #[SensitiveParameter] proteggono gli stack trace ma non le stringhe che si costruiscono autonomamente.

In ambito: un avversario che ottiene il file cifrato ma non le password — non può leggere il contenuto (a seconda della robustezza della password) e il file non rivela testo in chiaro. Fuori ambito: un avversario che dispone della password utente o proprietario; un lettore non conforme che ignora i bit di autorizzazione; la forza bruta offline di una password debole; il rilevamento delle manomissioni (la cifratura fornisce riservatezza, non integrità); i canali laterali nella build OpenSSL dell’host; e la custodia delle chiavi, che resta interamente responsabilità dell’integratore. La documentazione di queste minacce non equivale ad affermare l’assenza di vulnerabilità.

Le primitive crittografiche sono fornite dalla build OpenSSL dell’host, quindi la postura FIPS è una proprietà dell’host, non un’impostazione della libreria. CryptoCapabilities::detectFipsMode() restituisce un FipsModeDetection a tre stati (src/Security/FipsModeDetection.php): FIPS_ACTIVE, FIPS_ABSENT oppure INDETERMINATE. L’estensione openssl di PHP non espone alcun binding per il modello di provider di OpenSSL 3, quindi il rilevamento è best-effort; INDETERMINATE viene trattato come “FIPS non comprovato” (fail-closed), distinguibile nella telemetria per gli operatori. NextPDF non dichiara la validazione FIPS 140; l’esecuzione su un OpenSSL validato FIPS è responsabilità dell’operatore e il risultato del rilevamento è indicativo.

DichiarazioneSpecificaClausolareference_id
Il codice V del dizionario di cifratura seleziona l’algoritmo di cifratura.ISO 32000-2§7.6
Il metodo del crypt filter AESV3 è denominato dalla voce CFM.ISO 32000-2§7.6
La voce P è una quantità di autorizzazione di accesso a 32 bit senza segno.ISO 32000-2§7.6
Il bit di autorizzazione 3 controlla la stampa.ISO 32000-2§7.6
Il bit di autorizzazione 6 controlla l’annotazione / la compilazione dei moduli.ISO 32000-2§7.6
La cifratura protegge il contenuto dall’accesso non autorizzato (riservatezza).ISO 32000-2§7.6
La derivazione della chiave della revisione 6 usa un hashing iterativo con salt (Algoritmo 2.B).ISO 32000-2§7.6
CBC è una modalità di riservatezza (non una modalità di integrità).NIST SP 800-38A§6.2
I vettori di inizializzazione CBC devono essere imprevedibili.NIST SP 800-38AApp. C

NextPDF implementa le clausole citate; non dichiara una conformità generale a ISO 32000-2, la validazione FIPS 140 né alcuna garanzia legale o contrattuale di riservatezza. Il “supporto del gestore di sicurezza standard” non equivale a una certificazione di sicurezza della propria distribuzione: dipende dalla custodia delle password e dalla policy del verificatore, esterne alla libreria.