HotPDF charge des PDF signés et chiffrés AES-256 en Delphi bien plus vite quil ne le faisait, parce que trois coûts séparés ont été retirés du chemin de chargement : une analyse de tokens sans tampon du flux source, un second passage de la vérification de mot de passe Algorithme 2.A dISO 32000-2 pendant le déchiffrement, et une boucle AES-128-CBC interne lente dans le hash durci R6. Sur les cibles Delphi, la seule correction R6 a fait passer le chargement dun document denviron 66 ms à 16 ms
Aucun de ceux-ci napparaissait sur les petits fichiers de test que la plupart des suites emploient. Un échantillon non signé et non chiffré dun ou deux kilooctets souvre en quelques centièmes de seconde quoi que fasse lanalyseur. Les ennuis commencent avec les documents que les clients envoient réellement : un contrat avec une signature CMS incorporée, environ 35 Ko, protégé par un mot de passe de révision 6. Cette combinaison allume à la fois chaque chemin de code coûteux, et chacun avait sa propre raison dêtre plus lent que nécessaire. Cet article les passe en revue dans lordre où ils ont été trouvés, avec les erreurs de clé de cache qui sont apparues quand les résultats de vérification de signature ont commencé à être mémoïsés
Pourquoi un PDF signé de 34 Ko prend-il 170 ms à charger ?
Un PDF signé de 34 Ko prenait 170 ms à charger parce que lanalyseur lisait directement un THandleStream nu, et chaque octet de token devenait son propre appel système ReadFile. Un tokenizer PDF avance un octet à la fois pendant quil cherche des délimiteurs, si bien quun flux de handle sans tampon convertit un balayage bon marché en mémoire en milliers de transitions noyau
Les détails expliquent pourquoi le bug a survécu. THotPDF.LoadFromFile enveloppe sa source de fichier dans un tampon de lecture interne depuis longtemps, et la sonde AutoDecrypt avait déjà été corrigée pour la même raison après avoir passé 1,4 s à réinterroger la taille dun fichier de 56 Mo. Les documents signés et chiffrés prennent un autre chemin, cependant. Pour être sûr que les lectures paresseuses dobjets et linspection de signatures voient toujours la même révision immuable, même après que lappelant libère son propre flux, THotPDF.LoadFromStream prend une photo de la source et lanalyse une seconde fois. Cette seconde analyse, comme le chargement de réemploi employé pour les révisions authentifiées, lisait le handle de photo sans tampon devant. Tous deux passent maintenant par le même lecteur à tampon interne quemploie le chemin fichier
Ce tampon est interne, donc vous ne le créez jamais vous-même. Ce que vous contrôlez, cest le flux que vous donnez à LoadFromStream, et HotPDF lit ce flux tel quel pendant la première passe structurelle. Si les données sont déjà en mémoire, passez un TMemoryStream. Si elles sont sur disque, appelez LoadFromFile, ou à partir de Delphi 10.1 Berlin donnez au chargeur un TBufferedFileStream plutôt quun TFileStream nu
uses
System.SysUtils, System.Classes, HPDFDoc;
function LoadEncryptedFromBytes(const FileName: string;
const Password: AnsiString): THotPDF;
var
Mem: TMemoryStream;
begin
Result := THotPDF.Create(nil);
Mem := TMemoryStream.Create;
try
Mem.LoadFromFile(FileName); // une lecture massive au lieu de lectures par token
Mem.Position := 0;
// LoadFromStream renvoie le nombre de pages, 0 quand le document na pas chargé
if Result.LoadFromStream(Mem, Password) < 1 then
raise Exception.Create('Could not load ' + FileName);
except
Result.Free;
Mem.Free;
raise;
end;
Mem.Free;
end;
Libérer le flux mémoire juste après le chargement est sûr pour une entrée signée et chiffrée, parce que le composant détient déjà sa propre photo des octets. Gardez quand même le fichier ou le tampon dorigine si vous prévoyez de vérifier des signatures ensuite, puisque la vérification hache les segments bruts /ByteRange du fichier plutôt que le modèle dobjets déchiffré
Mesurer chargement et vérification comme des phases séparées
Le temps de chargement et le temps de vérification de signature devraient être mesurés séparément, parce quils échouent de façons différentes et se corrigent à des endroits différents. La phase de chargement couvre lanalyse, la vérification de mot de passe et le déchiffrement. La phase de vérification rehache les intervalles doctets signés et vérifie la signature CMS. Si vous les pliez en un seul nombre, une régression de lun se cache dans lautre
Le TStopwatch de la RTL depuis System.Diagnostics suffit pour cela. Le harnais ci-dessous charge avec la surcharge mot de passe de THotPDF.LoadFromFile puis vérifie la première signature via la surcharge TStream de VerifyLoadedSignature. La surcharge flux est le bon choix pour un fichier chiffré, parce que le chemin de chargement par mot de passe ne garde pas de nom de fichier source pour que les surcharges sans paramètre le rouvrent
uses
System.SysUtils, System.Classes, System.Diagnostics,
HPDFDoc, HPDFSignVerify;
procedure TimeSignedEncryptedLoad(const FileName: string;
const Password: AnsiString);
var
Pdf: THotPDF;
Src: TFileStream;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
Watch: TStopwatch;
Pages: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Watch := TStopwatch.StartNew;
Pages := Pdf.LoadFromFile(FileName, Password);
Writeln(Format('Load + decrypt: %d ms, %d pages, encrypted=%s',
[Watch.ElapsedMilliseconds, Pages, BoolToStr(Pdf.IsEncrypted, True)]));
if (Pages < 1) or (Pdf.GetLoadedSignatureFieldCount = 0) then
Exit;
Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Watch := TStopwatch.StartNew;
Status := Pdf.VerifyLoadedSignature(0, Src, Info);
Writeln(Format('Verify signature 0: %d ms, status %d, signer %s',
[Watch.ElapsedMilliseconds, Ord(Status), string(Info.SignerName)]));
finally
Src.Free;
end;
finally
Pdf.Free;
end;
end;
Lancez-le plusieurs fois et ignorez la première passe. Le premier appel paie des coûts de configuration ponctuels, dont la construction des tables AES et lexécution de leur auto-test, et le cache disque fausse aussi la première lecture. Ce que vous cherchez est le nombre en régime permanent pour un vrai fichier client, mesuré contre une copie non signée et non chiffrée du même document
Pourquoi le mot de passe AES-256 était-il dérivé deux fois ?
HotPDF dérivait deux fois la clé de fichier AES-256 parce que deux étapes du même chargement exécutaient chacune la vérification complète du mot de passe de son côté. Avant lanalyse structurelle, RecoverReloadKeysFromStreamRanges pré-balaie les octets bruts pour que les conteneurs de flux dobjets chiffrés puissent être déchiffrés pendant que les filtres sont décodés. Ce pré-balayage exécute déjà les vérifications Algorithme 2.A dISO 32000-2 §7.6.4.3.3, validant le mot de passe contre les sels utilisateur et propriétaire et récupérant la clé de chiffrement du fichier. DecryptLoadedDocument ignorait alors ce résultat et refaisait toute la dérivation
Pour la révision 4 et plus ancien, le doublon ne coûtait guère plus quune boucle MD5. Pour la révision 6, lAlgorithme 2.A appelle le hash durci Algorithme 2.B, si bien que le doublon doublait la partie la plus chère de louverture du fichier. Réutiliser la clé récupérée est simple. La réutiliser en sécurité est la partie plus difficile, parce quune clé en cache qui survit à sa propre vérification de mot de passe devient un contournement dauthentification
La correction limite le réemploi avec un drapeau de portée vrai seulement entre le pré-balayage et lappel de déchiffrement à lintérieur des surcharges mot de passe de LoadFromFile et LoadFromStream, et effacé dans un bloc finally. Le drapeau nest posé que quand le pré-balayage a réellement validé le mot de passe. Un appel direct à DecryptLoadedDocument après un chargement simple exécute toujours la dérivation complète, parce que ce mot de passe na jamais été pré-balayé. Sans cette restriction, une clé de réemploi périmée dune ouverture antérieure réussie pourrait déchiffrer en silence le document pour un appelant ultérieur qui a fourni le mauvais mot de passe, et cest un bug bien pire quun chargement lent
Où le hash durci R6 passe-t-il son temps ?
Le hash durci R6 passe presque tout son temps dans le chiffrement AES-128-CBC. ISO 32000-2 §7.6.4.3.4 Algorithme 2.B répète un tour au moins 64 fois. Chaque tour construit un tampon de 64 copies du mot de passe, du hash courant et, côté propriétaire, de la chaîne /U, chiffre ce tampon en AES-128-CBC en employant les 16 premiers octets du hash comme clé et les 16 suivants comme IV, et choisit SHA-256, SHA-384 ou SHA-512 pour le prochain hash depuis les 16 premiers octets du chiffré lus comme un nombre modulo 3. Les hash sont courts. Les tampons chiffrés font plusieurs kilooctets par tour
Limplémentation de cette boucle par HotPDF, PDF20HashDanceR6, chiffrait le tampon un bloc à la fois au travers dun AES de référence orienté octets. Elle essaie maintenant dabord une routine CBC native sans remplissage, qui sur les cibles Delphi atteint le backend C AES lié. Avec ce changement, un chargement complet de document est tombé denviron 66 ms à 16 ms. Si la routine native est indisponible ou renvoie une longueur qui ne correspond pas, le code retombe sur la boucle de blocs historique, et cette boucle peut à son tour employer un bloc AES-128 piloté par tables dont les T-tables sont générées depuis la S-box existante. Le chiffre est le même dans chaque cas, si bien que la clé dérivée est identique octet pour octet quelle que soit la voie qui la calculée
Cet article parle de vitesse. Bien obtenir la règle de terminaison de lAlgorithme 2.B est un problème de correction séparé, et un décalage de un là rejette des mots de passe corrects seulement de temps en temps. Le côté configuration de R5 face à R6 est couvert dans le guide de configuration du chiffrement PDF AES-256 avec HotPDF
Quatre façons pour un bloc AES rapide de vous mentir
Un bloc AES piloté par tables peut produire un chiffré faux et passer quand même son auto-test. Chacun des quatre pièges ci-dessous est survenu pendant la construction de la voie rapide, et deux dentre eux sont passés au travers de lauto-test tel quil était conçu au départ
- Réentrance de lauto-test. La routine de préparation pose son drapeau dinitialisation avant dexécuter lauto-test, si bien quun appel réentrant fait pendant le test prend la voie de référence. La première version du test comparait donc limplémentation de référence avec elle-même. La correction est un bloc rapide brut sans garde que lauto-test appelle directement, si bien que la voie de tables est le code réellement testé
- Dispositions de tables échangées. Échanger lordre des octets de deux des quatre tables de tours (Te1 et Te3) produit un chiffré faux que lauto-test réentrant navait aucun moyen de remarquer. Ce qui la attrapé fut un programme de sonde autonome qui imprimait le chiffré FIPS-197 Annexe C.1
69c4e0d86a7b0430d8cdb78070b4c55apour la clé000102…0fet le clair00112233…ff. LAuto-test exige maintenant ce vecteur et légalité octet à octet avec le bloc de référence avant de faire confiance à la voie rapide - Contrôles de plage et de débordement FPC. Sous Free Pascal avec
-Cret-Co, les conversions explicitesByte()à la Delphi et larithmétique de décalage largeur octet lèvent des exceptions de plage ou de débordement. Les indices de tables et le doublement GF(2^8) sont maintenant calculés à largeur entière native et masqués avecand $FF - Mauvaise compilation de linliner FPC. De petits helpers de chargement et stockage big-endian qui prennent des paramètres
PByteétaient compilés en violations daccès quand marquésinlinesous FPC. Retirerinlineles a corrigés, et le coût de lappel est négligeable à côté des consultations de tables
La même expérience est derrière le câblage final. Router chaque bloc AES-128 de la bibliothèque au travers des tables rapides a causé une violation daccès FPC déterministe pendant un chargement candidat à la publication, si bien que ce changement a été annulé. La conception livrée est conservatrice : CBC natif dabord, une branche explicite de tables rapides à lintérieur de la boucle R6, et le bloc de référence partout ailleurs. La leçon générale est que du nouveau code cryptographique qui doit compiler sous plusieurs compilateurs a besoin dune sonde à réponses connues indépendante exécutée sur chaque compilateur cible. Un échec de test au niveau DLL ne peut pas vous dire si vous avez une table fausse ou un plantage. Pour en savoir plus sur le côté Free Pascal, voyez les notes sur les backends Deflate et AES de HotPDF sous Free Pascal
Comment mettre en cache la vérification de signature sans cacher la mauvaise réponse ?
Un cache de vérification de signature nest sûr que si sa clé couvre chaque entrée qui peut changer le verdict, y compris les entrées qui vivent hors des intervalles doctets signés. La couche document headless de HotPDF mémoïse les résultats de vérification CMS en hachant les octets CMS, les octets du dictionnaire de signature dans le gap /Contents, le contenu des intervalles couverts, une empreinte de politique, les budgets de vérification, la taille totale de la source et un seau temporel dune minute. Trois de ces entrées nont été ajoutées quaprès quun bug a montré quelles manquaient
Les fichiers de certificats de confiance sont indexés par contenu, pas par chemin. La couche opération écrit le même matériau PEM dans un nouveau nom de fichier temporaire pour chaque commande, si bien quune clé par chemin ne correspondait jamais et que le cache ne touchait jamais. La taille totale de la source a dû faire partie de la clé parce que le UnsignedTrailingBytes rapporté en dépend : une révision de base et une mise à jour incrémentale ultérieure peuvent partager exactement le même contenu couvert tout en ayant des longueurs finales différentes, et un cache indexé seulement sur les intervalles remettrait le verdict de la révision de base au fichier mis à jour. Le contexte de ce que signifient ces octets finaux est dans le guide HotPDF de vérification des signatures numériques PDF en Delphi
Le troisième bug était un piège du langage Pascal, pas une erreur de conception. Les routines de hash prennent un tampon const non typé, et passer une variable de tableau dynamique à un paramètre non typé passe ladresse de la référence du tableau, pas les données du tableau
function Checksum(const Data; Count: Integer): Cardinal;
var
P: PByte;
I: Integer;
begin
Result := 0;
P := @Data;
for I := 0 to Count - 1 do
begin
Result := Result + P^;
Inc(P);
end;
end;
procedure DigestCMS(const CMS: TBytes);
begin
// Faux : lit la référence de tableau et ce qui la suit sur la pile.
// Une lecture de 16 Ko depuis là sort de la page de pile et lève une
// violation daccès, et Delphi et FPC échouent à des points différents.
Writeln(Checksum(CMS, Length(CMS)));
// Juste : passer le premier élément, avec garde pour le tableau vide
if Length(CMS) > 0 then
Writeln(Checksum(CMS[0], Length(CMS)));
end;
Le verrou du cache est SyncObjs.TCriticalSection plutôt que lAPI Windows InitializeCriticalSection, parce que la même unité compile aussi sous FPC sur Linux, où cette API nexiste pas
Ce qui reste lent exprès
La dérivation de clé R6 elle-même nest jamais mise en cache entre chargements, et cest délibéré. Les octets externes et un mot de passe explicitement fourni passent toujours par le vrai chemin dauthentification, si bien que chaque chargement par mot de passe dun document R6 paie au moins un hash Algorithme 2.B, et à la fois un hash côté utilisateur et un côté propriétaire quand le mot de passe propriétaire est tenté. Le réemploi à portée décrit ci-dessus retire le doublon à lintérieur dun seul chargement et rien au-delà
- Le chiffre de 66 ms à 16 ms sapplique aux cibles Delphi avec le backend AES natif lié. Les builds FPC prennent le bloc piloté par tables, et la génération de code FPC sous Windows tournait 2 à 4 fois plus lentement que Delphi sous les mêmes assertions
- Le cache de vérification se lie à un seau temporel dune minute, si bien que des vérifications répétées des mêmes octets dans une fenêtre courte sont bon marché, tandis quun verdict en cache ne survit jamais à la minute où il a été calculé
- Un cache dautorité de révision fusionnée a été implémenté, débogué puis retiré. Il ne fermait pas lécart FPC et rendait les callbacks de sécurité Delphi mesurablement plus lents, bon rappel de mesurer avant de garder un cache
- La vérification de signature a besoin des octets du fichier chiffré dorigine, donc gardez la source disponible. Un déchiffrement plus rapide ne fait rien pour retirer cette exigence
FAQ
Réutiliser la clé pré-balayée affaiblit-il la vérification du mot de passe ?
Non. HotPDF réemploie la clé de fichier récupérée seulement à lintérieur du même appel surcharge mot de passe qui a validé le mot de passe avec les vérifications complètes de lAlgorithme 2.A, et le drapeau de portée est effacé dans un bloc finally. Un appel isolé à DecryptLoadedDocument exécute toujours la dérivation complète, si bien quune clé laissée par une ouverture antérieure ne peut pas être empruntée avec un mauvais mot de passe
Pourquoi ne pas mettre en cache le résultat du hash R6 entre chargements ?
Mettre le hash en cache entre chargements signifierait accepter un mot de passe sans le vérifier contre le fichier en cours douverture. HotPDF traite chaque source doctets externe et chaque mot de passe explicite comme une entrée non fiable qui doit sauthentifier par elle-même, si bien que lacélération mesurable est venue de rendre lAlgorithme 2.B plus rapide plutôt que de le sauter
Dois-je passer un TFileStream à LoadFromStream ?
Préférez LoadFromFile pour les fichiers sur disque, parce quil tamponne déjà en interne. Quand vous avez besoin de la surcharge flux, passez un TMemoryStream ou un TBufferedFileStream, puisque HotPDF lit directement le flux que vous fournissez pendant la première passe structurelle et quun flux de handle nu coûte un appel système par petite lecture
Chargement protégé par mot de passe, chiffrement AES-256 R6, vérification de signature CMS et le reste de lAPI de document chargé sont livrés ensemble dans une bibliothèque VCL native pour Delphi et C++Builder. Les détails de fonctionnalités et les versions dIDE prises en charge sont sur la page produit HotPDF Component