Articolo tecnico

Verificare cifratura e permessi PDF in Delphi

Un flag di permesso non è un meccanismo di sicurezza. Il bit che dice "vietata la copia" vive dentro lo stesso dizionario /Encrypt della crittografia, il che gli dà un'aria di applicabilità che non possiede, e nel momento in cui trattate le due cose come una sola la vostra verifica inizia a produrre risposte sbagliate. L'unica domanda che vale la pena porre a un PDF non è "è cifrato". È più specifica e più difficile: quale algoritmo, quale revisione del security handler, quale delle due password è stata impostata, quali bit di permesso vengono dichiarati e quali parti del file la cifratura tocca davvero. Un file può essere formalmente cifrato e praticamente aperto. Può rifiutarsi di essere letto e tuttavia lasciare i propri metadati in chiaro. Può bloccare la stampa con un flag che qualsiasi visualizzatore è libero di ignorare. Verificare un PDF significa risolvere tutti questi punti separatamente, e PDF Library for Delphi, il motore PDF di losLab per Delphi e C++Builder, espone ciascuno di essi sia tramite una API flat a handle interi sia tramite un livello a classi tipizzato

Che cosa registra davvero il dizionario /Encrypt

ISO 32000-1 §7.6 definisce la sicurezza del documento attraverso una manciata di voci di dizionario, e PDF Library for Delphi le rispecchia una a una nel record TPDFEncryption. La versione del filtro V e la revisione R selezionano la famiglia di algoritmi. Length porta la dimensione della chiave. I bit di permesso stanno in P, le stringhe di validazione della password proprietario e utente in O e U (con OE e UE aggiunti per AES-256), un flag EncryptMetadata viaggia accanto, e altri tre campi nominano i crypt filter applicati rispettivamente a stringhe, stream e file incorporati

Il valore di questo record sta nel fatto che non interpreta nulla al posto vostro. Restituisce il dizionario grezzo e lascia a voi trarre le conclusioni, che è esattamente ciò di cui una verifica ha bisogno. Il caso del testo in chiaro dentro un file cifrato affiora in StringFilterIdentity e StreamFilterIdentity: quando uno dei due è vero, i dati corrispondenti attraversano il filtro Identity intatti, indipendentemente da ciò che riporta lo stato di cifratura del documento. Uno scanner che si ferma alla presenza di un dizionario /Encrypt definirà protetto un file simile mentre le sue stringhe e i suoi stream se ne stanno in chiaro. La stessa sfumatura governa i metadati. Quando EncryptMetadata è falso il pacchetto XMP resta leggibile a qualsiasi indicizzatore mentre il contenuto delle pagine non lo è, cosa che conviene sapere nell'istante in cui le vostre regole di instradamento si basano su un campo titolo o autore

Diagramma di PDF Library for Delphi dei campi del dizionario PDF /Encrypt mappati sulle proprietà di verifica di TPDFEncryption comprese le trappole del crypt filter Identity
Ogni voce /Encrypt corrisponde a un campo di TPDFEncryption, e i flag del filtro Identity rivelano quali stringhe, stream o metadati restano leggibili a prescindere dallo stato di cifratura

Una breve sonda di sicurezza con l'API flat

Per la maggior parte delle pipeline, quattro chiamate flat rispondono alle domande di tutti i giorni. LoadFromFile restituisce 1 in caso di successo, e una volta aperto il documento gli ispettori di cifratura riportano rispetto al suo stato decifrato:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', UserPassword) <> 1 then
      raise Exception.Create('Open failed: wrong password or damaged file');
    Writeln('status    : ', PDF.EncryptionStatus);     // decifrato / cifrato / sconosciuto
    Writeln('algorithm : ', PDF.EncryptionAlgorithm);  // famiglia RC4 o AES
    Writeln('strength  : ', PDF.EncryptionStrength);   // classe di lunghezza della chiave
    Writeln('owner pw? : ', PDF.CheckPassword(CandidatePassword));
  finally
    PDF.Free;
  end;
end;

CheckPassword conta più di quanto la sua firma su una riga lasci intendere. Il PDF definisce due password con poteri diseguali. La password utente è necessaria per aprire il file. La password proprietario concede pieni diritti e scavalca ogni bit di permesso. I byte su disco sono identici in entrambi i casi, ma una sessione aperta con la password proprietario può fare cose che la sessione con password utente non può, quindi una verifica che non registra quale credenziale è stata presentata registra metà della verità. Il livello a classi rende la distinzione interrogabile. TPDFDocument.HasUserPassword e HasOwnerPassword riportano ciò che il file richiede, mentre IsUserPassword e IsOwnerPassword riportano quale password ha effettivamente aperto la sessione corrente. Registrate quel fatto nel log. Non registrate mai i valori delle password

La scala Strength, dove "AES-256" significa due cose

Le funzioni flat Encrypt e EncryptFile accettano un intero Strength con cinque valori significativi: 0 per RC4 a 40 bit, 1 per RC4 a 128 bit, 2 per AES a 128 bit leggibile da Acrobat 7, 3 per AES a 256 bit come introdotto con Acrobat 9, e 4 per AES a 256 bit come richiesto da Acrobat X e successivi

La parte interessante è che 3 e 4 sono entrambi etichettati AES-256 e non sono lo stesso schema. Strength 3 corrisponde alla revisione 5 del security handler, un progetto intermedio che Acrobat 9 distribuì e che ISO non adottò mai. Strength 4 corrisponde alla revisione 6, la cui funzione di derivazione della chiave è stata irrobustita e standardizzata in ISO 32000-2. Per un documento che state creando oggi non c'è alcun motivo di scegliere 3 invece di 4. Per una verifica la differenza è decisiva: una policy che recita "AES-256 secondo ISO 32000-2" è soddisfatta soltanto da R6, e un file R5 che si autodefinisce AES-256 viola quella policy pur superando un ingenuo controllo di robustezza. Il livello a classi tiene i due separati per nome, esAES256Bit per R5 contro esAES256BitAcroX per R6, e la proprietà EncryptionAcroX risponde alla domanda sulla revisione con un solo booleano

Scala Strength della cifratura PDF dal RC4 a 40 bit fino ad AES-256 revisione 5 contro revisione 6 per le verifiche in Delphi
Gli Strength 3 e 4 si chiamano entrambi AES-256, eppure solo la revisione 6 soddisfa una policy ISO 32000-2, quindi le verifiche devono registrare la revisione dell'handler anziché la sola etichetta

I bit di permesso e le loro clausole in piccolo sulla lunghezza della chiave

EncodePermissions impacchetta otto flag nell'intero che Encrypt e EncryptFile si aspettano. Stampa, copia, modifica e aggiunta di note compongono il set di base; compilazione dei campi, copia per accessibilità, assemblaggio e stampa a piena qualità compongono il set esteso. La clausola in piccolo, che la demo di cifratura della libreria stessa dichiara apertamente, è che i quattro estesi hanno effetto solo a partire dalla robustezza a 128 bit. Il flag di stampa a piena qualità ricade sotto la stessa regola: azzeratelo per forzare la stampa a bassa risoluzione e un documento a 40 bit vi ignorerà, perché anche quel declassamento richiede una cifratura a 128 bit o superiore. Codificate una policy di "sola stampa a bassa risoluzione" in un file a 40 bit e ogni visualizzatore stamperà comunque a piena qualità

La domanda più profonda è chi applichi mai questi bit, e la risposta è nessuno di cui possiate fidarvi. I permessi sono istruzioni per i lettori conformi, non restrizioni crittografiche. La chiave di decifratura è identica sia che la copia sia consentita sia che sia negata, quindi un set di permessi restrittivo si limita a mantenere onesti i visualizzatori onesti. Un lettore che sceglie di ignorare i bit non incontra alcun ostacolo crittografico. Se l'obbligo è impedire l'estrazione anziché scoraggiarla, il file ha bisogno di una password utente e il flusso di lavoro ha bisogno di controlli a livello di processo attorno a esso, e un rapporto di verifica dovrebbe indicare sotto quale dei due regimi ricade davvero ogni file, anziché trattare un flag di permesso come un lucchetto

Impostare la policy e dimostrare che ha attecchito

Applicare la cifratura a file esistenti non richiede di caricarli nell'albero degli oggetti. EncryptFile elabora l'input verso l'output in una sola chiamata, e il ciclo di verifica riapre il risultato per confermare che cosa sia finito su disco. La demo di cifratura inclusa segue la stessa forma scrivi-poi-rileggi:

var
  PDF: TPDFlib;
  R: Integer;
begin
  PDF := TPDFlib.Create;
  try
    R := PDF.EncryptFile('in.pdf', 'out.pdf', 'owner-secret', 'user-secret', 4,
      PDF.EncodePermissions(1, 0, 0, 0,    // stampa consentita; copia/modifica/note negate
                            0, 0, 0, 1));  // set esteso: solo stampa a piena qualità
    if (R = 1) and (PDF.LoadFromFile('out.pdf', 'user-secret') = 1) then
    begin
      Writeln('algorithm = ', PDF.EncryptionAlgorithm);
      Writeln('strength  = ', PDF.EncryptionStrength);
      Writeln('owner pw accepted: ', PDF.CheckPassword('owner-secret'));
    end;
  finally
    PDF.Free;
  end;
end;

I team che lavorano al livello del documento ottengono la stessa operazione con insiemi tipizzati al posto dell'impacchettamento di bit, cosa che sopravvive a una code review con molta meno fatica:

if not Doc.Encrypt('owner-secret', 'user-secret', esAES256BitAcroX,
  [ppCanPrint], [ppCanPrintFull]) then
  raise Exception.Create('Encryption failed');

In entrambi i casi, il passo di rilettura non è cerimonia opzionale. Intercetta gli errori di deployment che altrimenti affiorano mesi dopo sulla macchina di un cliente: una vecchia build della libreria che declassa silenziosamente la robustezza richiesta, un percorso di output mai scritto perché la directory era in sola lettura, un intero di permessi i cui argomenti sono entrati nell'ordine sbagliato. Tutti e tre superano uno smoke test locale e falliscono sul campo, e riaprire l'output trasforma ciascuno di essi in un'eccezione che vedete durante l'esecuzione che ha creato il file. GetEncryptionFingerprint restituisce un valore compatto che potete conservare con il record del job, così un confronto successivo può dire se due output condividono la stessa configurazione di cifratura senza riaprire nessuno dei due

Falsi positivi di verifica per cui vale la pena scrivere codice

Alcuni schemi spingono con regolarità gli scanner di sicurezza verso la conclusione sbagliata, e ciascuno nasce dal ridurre una domanda a più parti a una risposta sì o no. Il crypt filter Identity è l'esempio più limpido. Un dizionario /Encrypt è presente, il file si dichiara cifrato, eppure le stringhe e gli stream attraversano invariati il filtro Identity, quindi il contenuto reale è in chiaro. Leggere StringFilterIdentity e StreamFilterIdentity prima di dichiarare protetto qualsiasi cosa è il rimedio

La divisione dei metadati è più sottile. EncryptMetadata può discordare dal resto del documento in entrambe le direzioni, lasciando un file cifrato con un pacchetto XMP leggibile o, meno spesso, il contrario. "Il file è cifrato" non dice nulla sul fatto che lo siano i suoi metadati, cosa che conta nell'istante in cui un indicizzatore o una regola di instradamento va a prendere il titolo. I file incorporati aggiungono un terzo asse: il PDF consente un crypt filter dedicato solo agli allegati, quindi gli allegati possono essere l'unica parte cifrata di un documento altrimenti aperto, o l'unica parte in chiaro di uno cifrato. Catturate le tre assegnazioni di filtro come campi separati per stringhe, stream e file incorporati, e nessuna di queste trappole potrà cogliervi. Memorizzate un solo booleano e la chiamata sbagliata sarà solo questione di tempo

PDF Library for Delphi: flusso di verifica che controlla StringFilterIdentity, StreamFilterIdentity, EncryptMetadata e il crypt filter dei file incorporati prima di definire protetto un PDF
Quattro assi indipendenti decidono se un file dall'aspetto cifrato sia davvero sigillato, e ridurli a un solo booleano prima o poi classifica male un file

Rimuovere la cifratura, e sceglierla per i nuovi file

Una verifica si conclude spesso con la decisione di togliere la protezione, e la meccanica non è l'ostacolo. DecryptFile(InputFileName, OutputFileName, Password) scrive una copia decifrata senza un caricamento completo, e il metodo Decrypt sul documento caricato fa lo stesso in memoria quando un file è già aperto. Entrambi richiedono una password valida; nessuno dei due aggira la crittografia. Il vero cancello è la policy anziché il codice, quindi fate in modo che le vostre regole di accettazione dichiarino con chiarezza quando la rimozione è consentita e registrate la classe di password che l'ha autorizzata, perché il passo tecnico in sé non lascia alcuna traccia

La scelta per i nuovi output è più ristretta di quanto i cinque valori di Strength lascino intendere. Usate Strength 4, AES-256 revisione 6, a meno che non dobbiate aprire i file in visualizzatori più vecchi di Acrobat X. Strength 2, AES-128, è il pavimento pragmatico per un parco visualizzatori invecchiato che non può essere aggiornato. Le opzioni RC4 a 0 e 1 esistono perché possiate leggere e verificare archivi storici, non perché ci produciate qualcosa di nuovo; ricorrervi in un progetto del 2026 è il segno che un requisito a monte è ormai obsoleto

Lo stato di cifratura alimenta direttamente le decisioni sulla firma, poiché un workbench che valida e firma documenti ha bisogno della stessa disciplina di rilettura su cui questa verifica si appoggia. Quel terreno è trattato nell'articolo sul workbench di conformità e firma. Quando un lotto applica EncryptFile a migliaia di documenti di grandi dimensioni, la guida ad accesso diretto ai PDF di grandi dimensioni mostra come tenere piatta la memoria durante l'esecuzione. Il riferimento completo dell'API di cifratura si trova sulla pagina prodotto di PDF Library for Delphi