Vous chiffrez un rapport avec un masque censé tout autoriser, $FFFFFFF0, et l'onglet Sécurité d'Acrobat liste impression et modification du document comme non autorisés. /P est un mot de drapeaux non signé de 32 bits numérotés à partir de 1, si bien que ce masque efface impression (bit 3) et modification (bit 4) ; la valeur tout-autorisé est $FFFFFFFC, le point de départ de TPdfEncryptOptions dans PDFium Component
Comment le mot de drapeaux /P est disposé
Huit positions seulement portent du sens, les autres sont réservées, et le comptage démarre à 1. L'ISO 32000-1 §7.6.3.2 dit que P "doit être interprété comme une quantité non signée de 32 bits", numérotée de 1 au bout bas à 32 au bout haut, et demande aux lecteurs conformes d'ignorer tout bit autre que 3, 4, 5, 6, 9, 10, 11 et 12. L'ISO 32000-2 §7.6.4.2 corrige la Table 22 et épelle les positions réservées : les bits 1–2 doivent être 0, les bits 7–8 doivent être 1, et pour la révision 3 et après les bits 13–32 doivent être 1. PDF 2.0 prend aussi sa retraite au bit 10, que les lecteurs ignorent désormais et que les écrivains mettent toujours à 1 pour les vieux lecteurs ; l'exception accessibilité déménage au bit 5, si bien qu'un lecteur devrait agir comme si la copie était permise quand le contenu va à une technologie d'assistance. Une formule couvre chaque ligne ci-dessous. Le masque du bit n est 1 shl (n - 1) — et TPdfPermission se mappe sur le mot de la même façon
| Bit | Masque | Signification (révision 3 et après) | TPdfPermission |
|---|---|---|---|
| 1–2 | $00000003 | Réservé, doit être 0 | aucun |
| 3 | $00000004 | Impression, à une qualité qui dépend du bit 12 | permPrint |
| 4 | $00000008 | Modifier le contenu par des opérations autres que celles des bits 6, 9 et 11 | permModify |
| 5 | $00000010 | Copier ou extraire texte et graphiques | permCopy |
| 6 | $00000020 | Ajouter ou modifier des annotations, remplir des champs ; avec le bit 4, créer des champs | permAnnotate |
| 7–8 | $000000C0 | Réservé, doit être 1 | aucun |
| 9 | $00000100 | Remplir les champs de formulaire existants, y compris les champs de signature, même si le bit 6 est effacé | permFillForms |
| 10 | $00000200 | Extraction accessibilité ; inutilisé en PDF 2.0, toujours écrit à 1 | permAccessibility |
| 11 | $00000400 | Assembler : insérer, tourner ou supprimer des pages, créer des signets et vignettes | permAssemble |
| 12 | $00000800 | Imprimer une représentation fidèle et de haute qualité | permPrintHighQuality |
| 13–32 | $FFFFF000 | Réservé, doit être 1 (l'ISO/TS 32004 assigne le bit 13, voir plus bas) | aucun |
Le masque $FFFFFFF0 vient d'une lecture de "les bits bas sont réservés" et d'un nibble entier effacé par sécurité. La région réservée ne faisait que les bits 1–2, masque $3 ; effacer $F emporte aussi $4 et $8, qui sont impression et modification, et tout viewer qui honore les drapeaux refusera les deux. L'erreur en miroir est d'assembler le masque depuis zéro : OUez ensemble $4, $10 et $800 et vous obtenez un /P petit et positif avec les bits 7–8 et 13–32 effacés. La plupart des lecteurs ouvrent ce fichier sans mot, parce que la spécification leur dit d'ignorer les bits réservés, mais la valeur est non conforme pour la révision 3 et après et un vérificateur strict est en droit de se plaindre. Une troisième variante apparaît dans du code qui compte les bits depuis zéro, où "bit 3" devient $8 et le programme accorde la modification quand il visait l'impression. Aucune ne produit d'erreur à l'enregistrement. Les permissions sortent simplement différentes de celles que l'auteur voulait
/P -4 et /P 4294967292 sont-ils la même valeur ?
Oui. Les mêmes 32 bits, deux orthographes. Les objets entiers PDF sont signés, et la note juste au-dessus de la Table 22 souligne que, les bits hauts réservés valant 1, l'entier est écrit comme un nombre négatif ; -4 est la valeur conventionnelle tout-autorisé pour la révision 3 et après. Les producteurs qui gardent la valeur dans une variable non signée écrivent 4294967292 à la place, et les deux orthographes se rencontrent dans de vrais fichiers. La spécification tranche la bonne lecture dans sa propre dérivation de clé : l'étape (d) de l'Algorithme 2 de l'ISO 32000-1 §7.6.3.3 convertit P en nombre binaire non signé de 32 bits et passe les quatre octets au MD5 octet bas d'abord, si bien que pour les révisions 2 à 4 les valeurs -3904 et 4294963392 produisent la même clé de fichier. Un parseur qui stocke /P dans un Integer signé de 32 bits rejette l'orthographe non signée comme débordement ; passez la variable à Cardinal et l'orthographe négative échoue à la place, tandis que du code qui compare le texte du jeton à une valeur stockée finit par traiter un document comme deux. Lisez le jeton dans un Int64, acceptez tout de Low(Integer) à High(Cardinal), et masquez sur 32 bits avant toute comparaison. Le propre code de déchiffrement de PDFium Component lit /P ainsi pour les révisions 2 à 7, et son écrivain émet la forme signée que la note de la spécification montre. Le rapport de bits réservés du snippet stoppe au bit 14, puisque un bit 13 effacé est la façon dont l'ISO/TS 32004 marque un fichier qui porte un PDF MAC
uses
System.SysUtils;
// Normalise le texte du jeton /P venu de votre propre parseur de dictionnaire
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; ';
// Le bit 13 ($1000) est laissé de côté : l'ISO/TS 32004 l'efface pour exiger un PDF MAC
if (Mask and $FFFFE000) <> $FFFFE000 then
Result := Result + 'bits 14-32 not all set; ';
end;
// Attendu : les deux appels donnent $FFFFFFFC et un rapport de bits réservés vide
// TryNormalizePermissions('-4', M1);
// TryNormalizePermissions('4294967292', M2);
Construire un masque depuis un ensemble TPdfPermissions
Partez avec chaque bit réservé à sa valeur requise et OUez seulement ce que vous accordez, si bien qu'une permission oubliée ne peut que devenir un refus. TPdfEncryptOptions.Default dans l'unité FPdfEncrypt porte déjà $FFFFFFFC, qui autorise tout, et effacer un ou deux bits de lui suffit pour une restriction rapide. Une politique qui liste ce que les destinataires peuvent faire se lit mieux comme un ensemble, et le tableau constant ci-dessous s'aligne avec les positions de bits que TPdf emploie pour décoder le mot. Forcez le bit 10 à 1 quel que soit ce que l'appelant passe. L'ISO 32000-2 dit aux écrivains de toujours le poser, les lecteurs PDF 2.0 l'ignorent de toute façon, et le laisser effacé ne risque que de bloquer des lecteurs d'écran dans de vieux logiciels
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 et 13-32 posés ; bits 1-2 et chaque bit de permission effacés
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; // Revision = erR5 ici
Opts.UserPassword := ''; // tout le monde peut ouvrir le fichier
Opts.OwnerPassword := OwnerPassword; // garde l'ensemble de permissions
Opts.Revision := erR6;
Opts.Permissions := PermissionsToMask(
[permPrint, permPrintHighQuality, permFillForms]);
// Attendu : /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;
Attendu : le dictionnaire Encrypt de la sortie porte /P -1084, et un viewer qui honore les drapeaux offre l'impression pleine qualité et le remplissage de formulaires tout en grisant édition de contenu, copie, commentaires et assemblage de pages. Le bit 13 est une position que l'écrivain gère pour vous. Avec EnableIntegrityProtection laissé à False, SaveAsEncrypted pose le bit 13 quoi que dise votre masque ; avec lui à True, l'écrivain efface le bit 13 dans /P et /Perms ensemble. Les autres bits réservés restent votre responsabilité — d'où le point de départ $FFFFF0C0. L'exemple choisit erR6 parce que R6 est la révision AES-256 que l'ISO 32000-2 standardise ; l'enregistrement par défaut porte erR5
/Perms, la copie chiffrée de /P dans les fichiers AES-256
Aux révisions 2 à 4, le mot de permissions est soudé à la clé : changer un bit de /P et l'entrée MD5 change, la clé dérivée est fausse, et rien ne se déchiffre. Les fichiers AES-256 cassent ce lien. L'Algorithme 2.A de l'ISO 32000-2 §7.6.4.3.3 déballe la clé de chiffrement de fichier depuis /UE ou /OE, et P ne joue aucun rôle dans cette dérivation, si bien qu'à lui seul un /P en clair pourrait être édité librement. /Perms ferme le trou. L'Algorithme 10 (§7.6.4.4.9) remplit un bloc de 16 octets et le chiffre avec la clé de fichier en AES-256 mode ECB :
- octets 0–3 : le mot de permissions, octet bas d'abord, étendu à 64 bits par les octets 4–7, qui valent tous
$FF - octet 8 : ASCII
TouF, en accord avec/EncryptMetadata - octets 9–11 : le marqueur ASCII
adb - octets 12–15 : des données aléatoires que les lecteurs ignorent
L'étape (f) de l'Algorithme 2.A déchiffre le bloc, vérifie adb et exige que les octets 0–3 égalent P — shall, pas should. L'Algorithme 13 (§7.6.4.4.12) répète les vérifications de marqueur et de permissions avec le should plus mou et en ajoute une de plus : l'octet 8 contre EncryptMetadata. PDFium Component le lit à la stricte partout où son propre code ouvre la couche de chiffrement : déchiffrement AESV4, validation de PDF MAC et enregistrements incrémentaux dans des fichiers chiffrés. Après que le mot de passe user ou owner a déverrouillé la clé de fichier, ce code compare les quatre octets de permissions au /P normalisé, contrôle le remplissage $FF, l'octet T/F et le marqueur, et fait échouer le chargement avec "PDF encryption permissions validation failed" au moindre écart, puisque un désaccord signifie que quelqu'un a édité le dictionnaire après le chiffrement et qu'aucune des deux copies ne peut être prise au pied de la lettre
La comparaison EncryptMetadata mérite du soin dans tout lecteur que vous écrivez. Un raccourci comme Pos('/EncryptMetadata false', Dict) > 0 rate /EncryptMetadata false avec deux espaces, un saut de ligne entre clé et valeur, ou un commentaire entre les deux, tout cela étant de la syntaxe PDF légale. Le lecteur suppose alors le défaut true, trouve F dans un /Perms parfaitement valide, et rejette le fichier. Un fichier valide, refusé. Parsez la clé et sa valeur comme jetons, et traitez une valeur non booléenne comme mal formée plutôt que de lui substituer le défaut
L'ISO/TS 32004 donne au bit 13 un travail : quand il est effacé, le document porte un PDF MAC que les lecteurs doivent vérifier. Le jeton MAC se trouve dans le dictionnaire /AuthCode du trailer, qui n'est pas chiffré, si bien qu'une vérification qui ne cherche que /AuthCode conclut qu'un fichier avec lui supprimé n'a jamais été protégé en intégrité. Supprimer aussi /KDFSalt ne cache pas l'exigence, parce que le bit effacé vit dans /P et que /P est authentifié par /Perms. Le déchiffrement AESV4 de PDFium Component traite donc un bit 13 effacé, ou un /KDFSalt résiduel, sans /AuthCode comme un MAC amputé et échoue, et quand la protection en intégrité est désactivée, l'écrivain pose le bit 13 pour que sa sortie n'annonce jamais un MAC qu'elle n'a. La hiérarchie de clés et la plage d'octets derrière ce jeton appartiennent à le chiffrement AES-GCM et le MAC ISO/TS 32004 en PDF 2.0, et le hachage de mot de passe itéré qui déverrouille la clé de fichier avant que tout cela ne tourne est couvert dans le hachage de mot de passe R6 et son compte de rounds
Permissions ou UserPermissions : lequel votre UI doit-elle lire ?
Lisez Permissions pour décider ce que la session courante peut faire, et UserPermissions pour rapporter ce que le fichier accorde à un destinataire ordinaire. TPdf.Permissions enveloppe le FPDF_GetDocPermissions de PDFium et décrit l'accès accordé à ce document ouvert, ce qui dépend du mot de passe qui l'a déverrouillé. TPdf.UserPermissions enveloppe FPDF_GetDocUserPermissions et continue de rapporter la concession d'accès user depuis /P quel que soit le mot de passe employé, si bien que les deux peuvent différer quand le mot de passe owner a ouvert le fichier. Toutes deux sont des LongWord ; GrantedPermissions et UserGrantedPermissions les décodent en ensembles TPdfPermissions, et HasPermission et UserHasPermission testent un membre unique. SecurityHandlerRevision dit si le fichier a du tout un gestionnaire de sécurité. La routine ci-dessous imprime les deux vues entières du mot brut à côté de l'ensemble décodé, un moyen rapide de voir pourquoi un fichier "ne veut pas imprimer"
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; // à fixer avant 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;
// Attendu pour le fichier écrit plus tôt :
// R6 /P as Integer -1084, as Cardinal 4294966212 ($FFFFFBC4)
// permPrint True, permModify False, permCopy False, ...
La contrainte appartient à l'application hôte
PDFium rend, extrait du texte et rastérise des pages quoi que disent les drapeaux. Le moteur rapporte les permissions et laisse la politique à l'appelant. Quiconque tient le mot de passe user tient le contenu déchiffré — avec un mot de passe user vide, c'est tout le monde — si bien que la confidentialité ne peut venir que d'un mot de passe user que le lecteur n'a pas. Mettez la vérification dans chaque gestionnaire de commande (copier, exporter, suppression de page, impression) plutôt que de cacher des boutons de barre d'outils, puisque raccourcis, menus contextuels et automatisation contournent tous une barre d'outils, la même stratification que dans un volet de prévisualisation PDF sécurisé pour applications Delphi. Une commande d'impression, par exemple, teste permPrint avant de mettre quoi que ce soit en file
Dans un fichier AES-256, -1084 en texte clair ne prouve rien tant que les quatre premiers octets déchiffrés de /Perms ne lisent pas C4 FB FF FF. Cette comparaison octet pour octet tourne dans le code AESV4, PDF MAC et enregistrement incrémental chiffré du PDFium Component