Article technique

Ouvrir des PDF chiffrés avec une clé brute dans Delphi

PDF Library for Delphi peut ouvrir un PDF chiffré à partir de sa clé de fichier brute plutôt qu’avec un mot de passe. DAOpenFileWithEncryptionKey accepte la clé sous forme de texte hexadécimal, la vérifie contre le vérificateur déjà stocké dans le dictionnaire de chiffrement et renvoie un handle Direct Access en lecture seule ; DAOpenFromStreamWithEncryptionKey fait de même pour un TStream appartenant à l’appelant. Les deux sont arrivés dans la version 3.496.0

Le scénario est étroit, mais bien réel. Une mission de forensic vous remet une clé récupérée dans une image mémoire, sans le mot de passe. Un archivage en masse traite dix mille documents dont les clés de fichier se trouvent dans une base d’escrow, parce que le système DRM d’origine ne délivre plus de mots de passe depuis des années. Une migration depuis un produit de gestion des droits retiré dispose du matériel de clé, mais de rien d’autre. Dans tous ces cas, l’identifiant que vous détenez est le résultat de la dérivation de clé, pas son entrée, et aucun paramètre de mot de passe de l’API ne peut l’accepter

Pourquoi une clé de chiffrement de fichier n’est-elle pas un mot de passe ?

Un mot de passe et une clé de chiffrement de fichier se trouvent de part et d’autre de la dérivation de clé dans le gestionnaire de sécurité standard PDF (ISO 32000-1 §7.6.3). Le gestionnaire prend un mot de passe, le mélange avec /O, /P, l’identifiant du fichier et un hash propre à la révision, puis produit la clé de fichier. Introduisez une clé de fichier dans l’emplacement prévu pour le mot de passe et vous obtenez un non-sens haché en un autre non-sens ; c’est pourquoi il faut un point d’entrée dédié plutôt qu’un flag sur DAOpenFile

L’endroit où la clé brute peut être injectée est fixé par la révision. Les révisions 2 à 4 dérivent encore une clé distincte par objet à partir de la clé de fichier, du numéro d’objet, du numéro de génération et, pour AESV2, du sel AES ; détenir la clé de fichier ne permet donc pas du tout d’ignorer la dérivation au niveau de l’objet. Les révisions 5 à 7 utilisent directement la clé de fichier de 32 octets pour AES-256, sans étape par objet. Le seul niveau commun aux deux familles est la clé de fichier elle-même : c’est le seul endroit où PDF Library for Delphi accepte une clé fournie de l’extérieur. Si vous avez toujours le mot de passe, restez plutôt sur le chemin ordinaire et laissez le cycle de nouvelle tentative de mot de passe gérer une première tentative incorrecte, car le point d’entrée à clé brute abandonne délibérément plusieurs facilités conservées par le chemin avec mot de passe

Quelle entrée DAOpenFileWithEncryptionKey accepte-t-il ?

Uniquement de l’hexadécimal ASCII non préfixé, sans espaces et de longueur paire ; le nombre d’octets décodés doit correspondre exactement à la révision de chiffrement. Pour les révisions 2 à 4, la longueur attendue vient de /Length dans le dictionnaire de chiffrement : un multiple de 8 bits compris entre 5 et 16 octets, avec une valeur par défaut de 40 bits lorsque /Length est absent. Pour les révisions 5 à 7, elle est exactement de 32 octets, sans négociation. Un préfixe 0x, un nombre impair de chiffres, plus de 64 caractères hexadécimaux, un bit inconnu dans Options ou un document non chiffré produisent tous le même résultat : un handle égal à 0 et LastErrorCode défini sur PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, soit 425. Cette rigueur est intentionnelle. Un parseur permissif qui supprime les espaces et complète les entrées courtes avec des zéros pourrait transformer une clé tronquée copiée dans le presse-papiers en identifiant, puis échouer bien plus loin et de manière beaucoup moins lisible

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex contient 32 caractères hexadécimaux pour AES-128 R4, 64 pour AES-256 R6
    FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
    If FileHandle= 0 Then
      Raise Exception.CreateFmt('raw key refused (error %d)', [Lib.LastErrorCode]);
    Try
      PageRef:= Lib.DAFindPage(FileHandle, 1);
      WriteLn(Lib.DAExtractPageText(FileHandle, PageRef, 0));
    Finally
      Lib.DACloseFile(FileHandle);
    End;
  Finally
    Lib.Free;
  End;
End;

Les autres codes d’échec restent distincts afin qu’un traitement par lots puisse distinguer une erreur d’opérateur d’un problème dans les éléments de preuve : 411 lorsque le fichier n’existe pas, 401 lorsqu’il ne peut pas être ouvert en lecture et 409 lorsque la structure de références croisées est endommagée. Tout ce qui concerne la clé se réduit à 425, volontairement, car un point d’entrée à clé brute qui indiquera quelle partie de la clé était incorrecte deviendrait un oracle

Que prouve réellement la vérification ?

PDF Library for Delphi prouve que la clé fournie appartient à ce document en utilisant le vérificateur déjà présent dans le dictionnaire de chiffrement, et le contrôle diffère selon la révision. La révision 2 recalcule le chiffrement RC4 de la chaîne de bourrage standard de 32 octets et compare les 32 octets à /U. Les révisions 3 et 4 hachent le bourrage avec l’identifiant du fichier, exécutent la passe RC4 puis les 19 passes dérivées par XOR, et comparent les 16 premiers octets de /U. Les révisions 5 à 7 déchiffrent la chaîne /Perms de 16 octets avec un IV nul et vérifient simultanément quatre éléments indépendants : le mot de permissions little-endian par rapport à /P, les quatre octets FF aux positions 5 à 8, le flag de métadonnées chiffrées sous la forme T ou F, et le marqueur adb aux positions 10 à 12

Lorsqu’un vérificateur existe mais ne correspond pas, l’ouverture est refusée sans condition. Il faut le dire clairement, car c’est la garantie sur laquelle repose toute la fonctionnalité. Notez aussi ce que la vérification ne prouve pas : elle indique que la clé déchiffre ce fichier, pas que quelqu’un vous a autorisé à l’utiliser. Le mot de permissions récupéré de /Perms est un élément de preuve sur la clé, pas une autorisation, et si vous voulez savoir ce que le document prétend réellement autoriser, c’est une tâche distincte pour un audit du chiffrement et des permissions. Les problèmes de normalisation côté mot de passe, comme le traitement SASLprep des mots de passe AES-256 non ASCII, ne se présentent tout simplement pas ici, puisqu’aucune chaîne n’atteint un hash

Quand PDF_RAW_KEY_ALLOW_UNVERIFIED s’applique-t-il ?

PDF_RAW_KEY_ALLOW_UNVERIFIED couvre une seule situation : le document ne contient aucun vérificateur utilisable, parce que /Perms est absent ou ne fait pas 16 octets, ou parce que /U est trop court pour être comparé. Il ne peut pas passer outre une preuve qui échoue. Corrompez un chiffre hexadécimal dans /Perms et fournissez la bonne clé avec l’option activée : PDF Library for Delphi renvoie toujours 0 et 425. Fournissez une clé de 32 octets nuls face à un vérificateur intact avec l’option activée : la réponse est la même. L’option assouplit l’absence de preuve, jamais une contradiction avec celle-ci. Comme une ouverture de récupération et une ouverture vérifiée correspondent à deux états épistémiques différents, elles sont aussi signalées séparément au lieu d’être fondues dans la valeur de retour, et DAGetEncryptionKeyValidation prend le handle ouvert et renvoie l’une de trois constantes :

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) signifie qu’un vérificateur était présent et correspondait
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) signifie que la clé n’a été acceptée que parce qu’aucun vérificateur ne pouvait être évalué et que l’appelant a explicitement demandé cette politique
  • PDF_RAW_KEY_VALIDATION_NONE (0) est ce que renvoie un handle ouvert avec un mot de passe ordinaire
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // aucun vérificateur dans ce fichier ? réessayer selon une politique de récupération explicite
  FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex,
    PDF_RAW_KEY_ALLOW_UNVERIFIED);
If FileHandle<> 0 Then
Begin
  Case Lib.DAGetEncryptionKeyValidation(FileHandle) Of
    PDF_RAW_KEY_VALIDATION_VERIFIED:
      Chain.Note('key verified against the encryption dictionary');
    PDF_RAW_KEY_VALIDATION_UNVERIFIED:
      Chain.Note('no verifier available: extraction is unattested');
  End;
End;

Lecture seule par construction, et propriétaire du flux

Le point d’entrée fichier à clé brute ouvre toujours la source avec fmOpenRead or fmShareDenyWrite et marque toute la chaîne Direct Access comme étant en lecture seule ; DAAppendFile refuse donc l’écriture sur place et renvoie 2 au lieu de tenter une mise à jour incrémentielle. Ce n’est pas une politique dont le handle peut se défaire : elle est définie dans le constructeur avant même l’analyse du fichier. Pour un travail sur des éléments de preuve, la propriété recherchée est que les octets de la source soient identiques octet par octet après la fermeture du handle, et la suite de régression l’affirme exactement sur un fixture AES-128 de révision 4 comme sur un fixture AES-256 de révision 6. L’entrée par flux se comporte de la même façon pour les écritures et ajoute une règle : DAOpenFromStreamWithEncryptionKey ne prend jamais possession du flux, de sorte que DACloseFile laisse votre TStream vivant et que vous devez le libérer vous-même

Source:= TMemoryStream.Create;
Try
  Source.LoadFromFile(ArchivePath);
  Source.Position:= 0;
  FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
  If FileHandle<> 0 Then
  Try
    // 2 = handle en lecture seule : exporter ailleurs, ne jamais ajouter à la preuve
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // le handle n’a jamais possédé ce flux
End;

Hygiène de la clé et exports de la DLL

La fermeture de la chaîne écrase la clé de fichier, le cache du mot de passe et les clés d’objet dérivées, et la clé décodée est effacée dans le bloc Finally du point d’entrée lui-même, que l’ouverture ait réussi ou non. Une subtilité derrière ce comportement a coûté un vrai temps de débogage : toute copie qui doit survivre est clonée explicitement avec SetLength et Move plutôt qu’assignée. Affectez un AnsiString à un autre dans Delphi et les deux noms partagent un tampon en copy-on-write ; effacer le côté appelant mettrait donc à zéro la clé que le gestionnaire cryptographique utilise encore, et le document serait déchiffré en données incohérentes sans qu’aucune trace de pile ne puisse l’expliquer. Seuls les points d’entrée fondés sur un fichier franchissent la frontière DLL, sous leurs formes wide et ANSI, avec l’accesseur de statut de validation ; la variante TStream reste réservée à Delphi, car elle dépend de la durée de vie des objets Delphi et de sémantiques de référence qui n’ont pas de représentation honnête dans une ABI C plate. Si votre outil de récupération est un client DLL, prévoyez de mettre en scène les données dans un fichier temporaire et de le supprimer selon les mêmes contrôles que ceux appliqués à la clé

Traitez la clé hexadécimale comme un identifiant sensible, avec les règles de manipulation que vous appliqueriez à un mot de passe de document, et conservez le statut de validation dans le journal produit par votre chaîne de conservation, afin qu’un lecteur ultérieur puisse distinguer une extraction vérifiée d’une extraction non attestée. Si vous évaluez un composant PDF Delphi pour de la forensic, de l’archivage en masse ou une migration DRM, les points d’entrée à clé brute, la garantie de lecture seule et la surface d’audit du chiffrement appartiennent tous à la même bibliothèque ; la liste complète des fonctionnalités figure sur la page produit PDF Library for Delphi