Le composant PDF HotPDF pour Delphi implémente le modèle de filtres cryptographiques de l’ISO 32000-1 §7.6.5 sous la forme de trois politiques indépendantes plutôt que d’un seul commutateur : ConfigureCryptFilterDefaults affecte séparément le filtre des chaînes /StrF, le filtre des flux /StmF et le filtre des fichiers incorporés /EFF, SetStreamCryptFilter remplace le filtre d’un seul flux et GetLoadedCryptFilterInfo indique ce que déclare un fichier entrant. La plupart des bugs d’interopérabilité des PDF chiffrés vivent dans les espaces entre ces trois éléments
Voici l’échec qui conduit les équipes dans cette couche. Une équipe livre un document dont le contenu des pages doit rester lisible par un outil en aval, mais dont la charge jointe ne doit pas l’être ; elle définit donc /EFF /StdCF et laisse /StmF /Identity. Acrobat l’ouvre sans problème. Un lecteur tiers conforme renvoie la pièce jointe sous forme de charabia chiffré, car /EFF est une politique côté producteur qui indique quel filtre s’applique aux fichiers incorporés, tandis qu’un lecteur général résout toujours un flux non marqué via /StmF. Le correctif n’est pas une autre valeur /EFF. Il consiste à placer un filtre /Crypt explicite sur le flux du fichier incorporé lui-même
Ce que contrôle réellement la couche de filtres cryptographiques
Les filtres cryptographiques se placent entre l’algorithme de chiffrement et le graphe d’objets ; ils décident quels objets l’algorithme touche, pas comment il fonctionne. Le dictionnaire /CF du dictionnaire de chiffrement associe des noms à des définitions de filtres, chacune portant une méthode /CFM, une /Length facultative et un /AuthEvent. Les trois entrées de premier niveau /StrF, /StmF et /EFF sélectionnent ensuite lequel de ces filtres nommés s’applique aux chaînes, aux flux sans filtre explicite et aux fichiers incorporés. HotPDF limite délibérément ce que ses gestionnaires intégrés vont écrire. ConfigureCryptFilterDefaults n’accepte que les noms réservés du gestionnaire actif : le gestionnaire de sécurité Standard émet /StdCF ou /Identity, le gestionnaire à clé publique émet /DefaultCryptFilter ou /Identity, et toute autre valeur lève EArgumentException au point d’appel. Les filtres écrits par des producteurs externes sous d’autres noms sont tout de même conservés sur les chemins de chargement, d’inspection et de réécriture de compatibilité ; HotPDF est donc conservateur comme écrivain et permissif comme lecteur. Deux garde-fous supplémentaires s’appliquent : l’appel lève EInvalidOpException dès que la sérialisation du document a commencé, ainsi que si le document est en mise à jour incrémentielle, car la politique de chiffrement ne peut pas changer entre deux révisions du même fichier
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'wrapper.pdf';
Pdf.OwnerPassword := 'owner-secret';
Pdf.UserPassword := 'open-secret';
Pdf.CryptKeyLength := aes128;
// chaînes chiffrées, flux de page en clair, pièces jointes chiffrées
Pdf.ConfigureCryptFilterDefaults('StdCF', 'Identity', 'StdCF');
Pdf.ActivateProtection := True;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(50, 50, 0, 'Visible stream operators');
Pdf.AddDocumentAttachment('payload.bin', 'Encrypted payload');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Une contrainte mérite d’être énoncée dès le départ, car elle est vérifiée tardivement et surprend. Les filtres cryptographiques nommés de HotPDF exigent un chiffrement de document aes128, aes256 ou aesgcm. Configurez une politique de filtre au-dessus de RC4 k40 ou k128 et la passe de validation exécutée lorsque le chiffrement est activé lève une exception au lieu de promouvoir silencieusement le type de clé. C’est la même posture de conception que pour le reste du chemin de chiffrement PDF AES-256 dans Delphi : refuser la configuration ambiguë plutôt que deviner ce que l’appelant voulait dire
Pourquoi l’entrée /Length a-t-elle deux significations ?
Parce que la spécification la définit dans deux unités différentes selon le gestionnaire de sécurité, et que HotPDF doit respecter les deux. Dans un dictionnaire de filtre cryptographique dont le /CFM est /V2, l’entrée /Length est exprimée en octets avec le gestionnaire Standard et en bits avec le gestionnaire à clé publique. L’entrée /Length du dictionnaire de chiffrement, placée à côté de /V (ISO 32000-1 §7.6.2), est toujours en bits. Lisez un dictionnaire de filtre contenant /Length 16 : vous avez une clé de 128 bits dans un fichier avec gestionnaire Standard et un fichier rejeté dans un fichier à clé publique. HotPDF normalise cette valeur lorsqu’il capture la configuration chargée. Il multiplie par huit le /Length d’un filtre /V2 uniquement lorsque le fichier n’est pas chiffré par clé publique, revient au /Length du document lorsque le filtre omet le sien et stocke le résultat dans THPDFCryptFilterInfo.KeyLengthBits. AESV2 est fixé à 128 bits, et AESV3 ainsi que AESV4 à 256, car ces méthodes n’ont pas de taille de clé négociable. La partie stricte vient ensuite : seuls les /V2 de 40 et 128 bits sont acceptés. Un filtre qui se résout vers une autre longueur est signalé comme indisponible et l’opération échoue, au lieu d’être arrondi à 128 selon l’idée que la plupart des producteurs voulaient probablement dire 128. Normaliser silencieusement une longueur de clé est la manière de livrer un fichier qui se déchiffre sur votre machine et nulle part ailleurs
var
Reader: THotPDF;
Info: THPDFCryptFilterInfo;
I: Integer;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
if Reader.LoadFromFile('incoming.pdf', 'open-secret') <> 1 then
Exit;
// /StrF et /StmF valent par défaut Identity ; /EFF vaut par défaut /StmF
WriteLn(Reader.LoadedStringCryptFilterName); // StdCF
WriteLn(Reader.LoadedStreamCryptFilterName); // Identity
WriteLn(Reader.LoadedEmbeddedFileCryptFilterName); // StdCF
for I := 0 to Reader.GetLoadedCryptFilterCount - 1 do
if Reader.GetLoadedCryptFilterInfo(I, Info) then
if (Info.Method = hcfmV2) and
not (Info.KeyLengthBits in [40, 128]) then
raise Exception.CreateFmt(
'crypt filter /%s: unsupported V2 key length %d',
[String(Info.Name), Info.KeyLengthBits]);
finally
Reader.Free;
end;
end;
Que garantit /CFM /None et en quoi /Identity diffère-t-il ?
Ils aboutissent au même résultat par des chemins différents, et les confondre casse les recherches. Un filtre nommé dont le /CFM vaut /None et un filtre nommé qui omet entièrement /CFM signifient tous deux que ce filtre n’effectue aucun chiffrement ni déchiffrement — HotPDF associe l’entrée manquante à None avant la résolution, de sorte que les deux aboutissent à hcfmNone avec une longueur de clé enregistrée à zéro. /Identity est différent par nature : c’est le nom réservé qui contourne complètement la recherche dans /CF, de sorte qu’un document peut référencer /Identity sans le définir nulle part dans /CF. Les noms PDF sont sensibles à la casse, ce qui rend un autre détail d’implémentation non négociable : aucune recherche de filtre cryptographique ne doit ignorer la casse. HotPDF résout les noms du sous-dictionnaire /CF, l’entrée /Length du filtre et le contrôle /Type du flux avec des recherches de dictionnaire sensibles à la casse. Un fichier qui définit /stdcf alors que /StmF pointe vers /StdCF est mal formé, et traiter les deux comme la même clé transformerait une erreur d’écriture détectable en mauvaise clé appliquée silencieusement à chaque flux du document
Faire respecter /EFF sur les flux de fichiers incorporés
Lorsque /EFF diffère de /StmF, le flux du fichier incorporé a besoin d’une entrée /Crypt explicite en tête de son /Filter et d’un dictionnaire /DecodeParms correspondant qui porte /Name à la même position dans le tableau. HotPDF détermine cela pour chaque flux au moment de la sauvegarde : il détecte /Type /EmbeddedFile, hérite du filtre de fichier incorporé configuré et n’émet le marqueur /Crypt explicite que lorsque ce nom hérité diffère de la valeur par défaut effective du flux. Lorsque /EFF et /StmF concordent, aucun marqueur n’est écrit, car un lecteur résoudrait de toute façon le même filtre. La position dans le tableau compte alors autant que le nom. Lorsqu’il relit un flux, HotPDF parcourt /Filter pour trouver l’entrée /Crypt, enregistre son index puis recherche ce même index dans le tableau /DecodeParms afin d’y trouver /Name. Un /Crypt à l’index 0 associé à des paramètres à l’index 1 se résout en /Identity, pas en votre filtre. C’est aussi pourquoi l’écrivain complète le tableau des paramètres par un null lorsque le flux possédait auparavant un /Filter mais aucun /DecodeParms : les positions doivent rester alignées
Un piège plus aigu se cache dessous. Si le /Filter ou le /DecodeParms existant est un objet indirect — cas courant dans les fichiers issus de générateurs qui partagent un même tableau de filtres entre plusieurs flux — l’insertion de /Crypt sur place modifierait un graphe de filtres partagé et corromprait tous les autres flux qui le référencent. HotPDF résout l’objet indirect et le clone d’abord en objet direct privé au flux, en effaçant les numéros d’objet et de génération afin que la racine indirecte d’origine ne soit jamais intégrée au nouveau tableau. Pour un flux qui utilisait déjà ASCIIHexDecode, le résultat sérialisé est /Filter [ /Crypt /ASCIIHexDecode ] avec /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. La même discipline positionnelle gouverne toute autre chaîne de filtres, y compris celles parcourues lors de l’extraction d’images d’un PDF chargé à travers leurs filtres de décodage
// L’éditeur possède déjà un document chargé, et ContentStream est un
// THPDFStreamObject dont le /Filter est un nom /ASCIIHexDecode indirect
Editor.OwnerPassword := 'owner-secret';
Editor.UserPassword := 'open-secret';
Editor.CryptKeyLength := aes128;
Editor.ConfigureCryptFilterDefaults('StdCF', 'Identity');
Editor.SetStreamCryptFilter(ContentStream, 'StdCF');
Editor.ActivateProtection := True;
Editor.SaveLoadedDocument('out.pdf');
// Un nom vide efface le remplacement et retire l’entrée /Crypt obsolète
// ainsi que ses paramètres de décodage lors de la sauvegarde suivante
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');
Les flux d’objets héritent-ils de la politique /Encrypt du document ?
Non, et le supposer est une manière fiable de produire du contenu incohérent. Un flux d’objets doit suivre la politique /StmF effective ou son propre marqueur /Crypt explicite : la simple présence d’un dictionnaire /Encrypt ne transforme pas chaque conteneur /ObjStm en texte chiffré. Un document avec /StmF /Identity possède des flux d’objets en clair même si ses chaînes sont entièrement chiffrées, et un décodeur qui les déchiffrerait malgré tout fournirait à l’étape inflate des données qui ne sont jamais sorties de deflate
La conséquence pour les objets membres mérite d’être relue deux fois. Selon l’ISO 32000-1 §7.5.7, les chaînes à l’intérieur d’un flux d’objets chiffré sont déjà en clair une fois le conteneur déchiffré ; les déchiffrer à nouveau ferait donc un double déchiffrement. HotPDF s’en protège en demandant si le conteneur de chaque objet de type 2 était chiffré et en ignorant l’objet dans ce cas, tout en comptant les sauts dans XRefProbeDecryptObjStmSkips comme preuve directe que la garde s’est déclenchée. Lorsque le conteneur était en clair, les chaînes membres n’étaient couvertes par rien ; HotPDF matérialise donc ces membres et applique /StrF à chacun individuellement, avec une clé basée, comme le fait réellement l’implémentation, sur le numéro d’objet et la génération du membre, pas sur le numéro d’objet /ObjStm conteneur. Inversez cette règle sur un fichier à politiques mixtes et chaque chaîne de chaque objet compressé se décode en bruit. Les règles de niveau conteneur sont détaillées davantage dans les notes sur les flux d’objets PDF et les mises à jour incrémentielles
Les endroits où HotPDF refuse de deviner
La sémantique des filtres cryptographiques n’existe pas sous /V 4 ; HotPDF rejette donc tout remplacement par flux sur un tel fichier avec une erreur explicite, plutôt que d’écrire un marqueur /Crypt qu’aucun lecteur conforme ne respecterait. Il en va de même à la lecture : un dictionnaire de chiffrement dont /V est inférieur à 4 efface les trois noms de filtres chargés, car il n’y a rien à signaler. Trois autres limites sont appliquées délibérément :
- Un filtre par flux différent de
Identitysur un document chiffré par clé publique est refusé, car une politique propre au flux sous le gestionnaire à clé publique exige une enveloppe de destinataire propre au flux que HotPDF n’émet pas encore - Les fichiers incorporés chiffrés par clé publique dont
/EFFdiffère de/StmFeffectif sont refusés pour la même raison, plutôt que d’être écrits dans une forme que personne ne puisse déchiffrer - Le chemin rapide de fichier direct AES-256 ne s’applique que lorsque les chaînes, les flux et les fichiers incorporés se résolvent tous vers la même méthode de filtre cryptographique et qu’aucun objet du fichier ne porte de
/Cryptexplicite ; une politique mixte ou des métadonnées en clair force le repli vers le chemin complet du graphe d’objets
Aucune de ces limites n’est une décision de performance. Elles marquent les endroits où une mauvaise supposition produit un PDF qui s’ouvre dans un viewer, échoue dans un autre et ne donne aucun signal au développeur avant qu’un client ne le signale. Un refus lors de ConfigureCryptFilterDefaults ou au moment de la sauvegarde coûte une exception ; un fichier incorporé chiffré avec la mauvaise clé en silence coûte un cycle de support. Si vous développez en Delphi ou C++Builder un logiciel qui produit ou consomme des PDF chiffrés — contenu de page sélectivement en clair avec pièces jointes chiffrées, enveloppes de charges chiffrées PDF 2.0 ou interopérabilité avec des fichiers dont vous n’avez pas choisi les politiques de filtres cryptographiques — l’API de filtres cryptographiques décrite ici est livrée dans l’actuel composant PDF HotPDF pour Delphi, avec les chemins de chiffrement, de flux d’objets et de mise à jour incrémentielle sur lesquels elle s’appuie