يشفر HotPDF ملف PDF لحاملي شهادات محددين عبر معالج أمن المفاتيح العامة في ISO 32000: تأخذ EnablePubKeyEncryption بذرة عشوائية من 20 بايت، ويحصل كل مستلم على مغلف CMS خاصته، يبنيه AddPubKeyRecipientCertificate لمفاتيح RSA (نقل مفاتيح RSA-OAEP) أو AddPubKeyAgreementRecipientWithSecret للمفاتيح الإهليلجية (ECDH على P-256 أو P-384 أو P-521 أو X25519 أو X448). لا أحد يشارك كلمة سر؛ ومن يملك مفتاحاً خاصاً مطابقاً يفتح الملف
حالة الاستخدام هي دائماً صيغة من القصة نفسها. حزمة تدقيق ربع سنوية تذهب إلى ثلاثة مراجعين خارجيين، والشؤون القانونية تريد أن يقرأها كل منهم، وواحد منهم فقط يجوز له أن يطبعها، ولا أحد يريد كلمة سر جالسة في سلسلة بريد بجوار المرفق. تشفير كلمات السر لا يستطيع التعبير عن ذلك. تشفير الشهادات يستطيع، لأن كل مستلم يفتح المستند بمفتاح يملكه أصلاً، وكل مستلم يمكن أن يحمل مجموعة صلاحيات مختلفة داخل مغلفه الخاص
بماذا يختلف تشفير PDF بالشهادات عن كلمة السر؟
يشتق ملف PDF المشفر بمفتاح عام مفتاح ملفه من بذرة عشوائية زائد البايتات الدقيقة لكل مغلف مستلم، لا من أي شيء يكتبه إنسان. المعالج موصوف في §7.6.4 من ISO 32000-1 (و §7.6.5 في ISO 32000-2)، والمغلفات بنى CMS من نوع EnvelopedData كما يعرفها RFC 5652. تكتب HotPDF /Filter /Adobe.PubSec مع /SubFilter /adbe.pkcs7.s5؛ ولـ AES-256 يعني ذلك /V 5 ومدخل /DefaultCryptFilter تحت /CF بـ /CFM /AESV3، وتسكن مصفوفة /Recipients داخل مرشح التشفير ذاك. يشفّر كل مغلف 24 بايت: البذرة ذات الـ 20 بايت يليها كلمة صلاحيات الـ 32 بت الخاصة بذلك المستلم. وقيمة /P في قاموس التشفير مجرد عنصر نائب، لأن الصلاحيات الحقيقية تسافر داخل كل مغلف. عند التحميل يفك القارئ مغلفاً واحداً، ويستعيد البذرة، ويبصم البذرة مع كل مغلف بترتيب /Recipients (SHA-256 لـ AES-256 و SHA-1 للشيفرات الأقدم) لإعادة بناء مفتاح الملف. وإن كنت ما زلت تقرر بين هذا النموذج وكلمات السر العادية، يغطي دليل تشفير AES-256 بكلمات السر وأعلام الصلاحيات الوجه الآخر من تلك المقايضة
كتابة مستلمي RSA عبر EnablePubKeyEncryption
لشهادات RSA، استدعِ EnablePubKeyEncryption بـ aes256، ثم استدعِ AddPubKeyRecipientCertificate مرة عن كل شهادة مرمزة DER قبل BeginDoc. تبني المساعدة مغلف RSAES-OAEP داخل العملية بقيم THPDFRSAOAEPHash لهضم OAEP وهضم MGF1 (rohSHA256 أو rohSHA384 أو rohSHA512)، وتشفر محتوى المغلف بـ AES-256-CBC
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFRSA;
procedure WriteAuditPack(const OutFile: string);
var
Pdf: THotPDF;
Seed: AnsiString;
begin
SetLength(Seed, 20); // 20 بايتاً بالضبط، حتى لـ AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // نوع المفتاح الافتراضي aes128
// المراجع A يجوز له أن يطبع؛ والمراجع B يقرأ ويستخرج فقط
Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-a.cer'),
[prPrint, prPrint12bit, prExtractContent], rohSHA256, rohSHA256);
Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-b.cer'),
[prExtractContent]);
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 720, 0, 'Q3 audit pack');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
ثلاثة تفاصيل في ذلك السرد تحمل البناء. أولاً، طول البذرة مثبت عند 20 بايتاً لكل نوع مفتاح، AES-256 ضمناً؛ وترفع EnablePubKeyEncryption استثناء عند أي طول آخر. ثانياً، يفترض EnablePubKeyEncryption افتراضياً aes128، وكلا مساعدتي الشهادات يرفضان العمل ما لم يكن نوع المفتاح aes256، فنسيان الوسيط الثاني يجلب لك الاستثناء «مغلفات الشهادات تتطلب aes256». الشيفرات القديمة (k40 و k128 و aes128) ما زالت تعمل، لكن عبر AddPubKeyRecipient فقط وبمغلف بنيتَه في مكان آخر. ثالثاً، تشفير المفتاح العام بـ AES-256 خاصية من PDF 2.0، فترفع HotPDF نسخة المستند إلى 2.0 تلقائياً. وضبط StrictVersionLock على نسخة أدنى يجعل EnablePubKeyEncryption تعود دون تمكين أي شيء، ولا يظهر الفشل إلا في السطر التالي بصيغة «استدعِ EnablePubKeyEncryption أولاً». وتبديل التشفير أثناء تحديث تزايدي يرفع EInvalidOpException فوراً
إضافة مستلمي ECDH: P-256 و P-384 و P-521 و X25519 و X448
لشهادات المنحنيات الإهليلجية تكتب AddPubKeyAgreementRecipientWithSecret مستلم اتفاق مفاتيح CMS (KeyAgreeRecipientInfo، بنية KARI من RFC 5753، مع مواصفة X25519 و X448 من RFC 8418) وتحسب السر المشترك ECDH داخل العملية. تختار المنحنى بقيمة THPDFPubKeyAgreementScheme: pkasECDHP256 أو pkasECDHP384 أو pkasECDHP521 أو pkasX25519 أو pkasX448. وعلى النمط أن يطابق المفتاح في الشهادة، وإلا رفع الاستدعاء «مفتاح الشهادة لا يطابق نمط الاتفاق المطلوب». تحت الغطاء يحصل كل مغلف على UKM عشوائي جديد من 32 بايت، ومفتاح تشفير مفاتيح مشتق بـ KDF من نوع stdDH (SHA-256 لـ P-256 و X25519، و SHA-384 لـ P-384، و SHA-512 لـ P-521 و X448)، ولف مفتاح AES-256 كما يعرفه RFC 3394. والسر المشترك نفسه يأتي من كود منحنيات Pascal صافٍ، دون أي مزود تشفير منصي؛ ومقالة حساب منحنيات NIST بـ Pascal الصافي تشرح كيف بُنيت تلك الطبقة وتحققت. ولمنحنيات Montgomery يمكن توليد زوج المفاتيح الزائل كله محلياً:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// scalar زائل جديد لكل مغلف؛ ويحدث الـ clamping داخل السلم
SetLength(Scalar, 32);
AESGenerateRandomBytes(@Scalar[0], Length(Scalar));
try
OriginatorPublic := HPDFX25519PublicFromScalar(Scalar);
Pdf.AddPubKeyAgreementRecipientWithSecret(
TFile.ReadAllBytes('legal-x25519.cer'),
[prPrint, prExtractContent], pkasX25519,
OriginatorPublic, Scalar,
[]); // OwnPublicPoint: ذات معنى لمنحنيات NIST فقط
finally
HPDFSecureClearBytes(Scalar);
end;
end;
منحنيات NIST تحتاج أكثر من المستدعي. تشحن HotPDF مساعدات مفاتيح عامة لـ X25519 و X448 فقط (HPDFX25519PublicFromScalar و HPDFX448PublicFromScalar)، فلـ P-256 و P-384 و P-521 تولّد زوج المفاتيح الزائل بأدواتك الخاصة وتمرر عدداً سلسلياً big-endian بحجم الحقل بالضبط (32 أو 48 أو 66 بايتاً) زائد النقطة غير المضغوطة المطابقة 0x04||X||Y بوصفها OriginatorPublicKey. تتحقق HotPDF من نقطة المستلم مقابل معادلة المنحنى، لكنها لا تستطيع فحص أن مفتاحك العام للبادئ يعود فعلاً إلى عدك السلسلي. النصفان غير المتطابقان ما زالا ينتجان مغلفاً سليم التكوين تماماً لا يستطيع أي مستلم فتحه، ولهذا تنتمي جولة تحميل ذهاباً وإياباً إلى مجموعة اختبارك، لا مجرد فحص حجم ملف
لماذا يهم ترتيب /Recipients؟
يهم ترتيب /Recipients لأن مفتاح الملف بصمة فوق البذرة وكل مغلف بترتيب المصفوفة، فيجب أن يبصم الكاتب والقارئ البايتات نفسها بالتتابع نفسه. تحفظ HotPDF المغلفات بترتيب إضافتك وتكتبها دون تغيير، أي يمكنك إضافة المستلمين بأي ترتيب تشاء، لكن لا يجوز لأي شيء في اتجاه المصب إعادة ترتيب تلك المصفوفة أو إعادة ترميزها أو «تنظيفها». معظم الأخطاء الحقيقية في هذا المجال كانت تنويعة على تلك الثيمة، حيث بصم الطرفان بايتات مختلفة قليلاً:
- تخزين المصفوفات الديناميكية في
TListعبرAddيحفظ مؤشراً خاماً فقط بينما يبقى العد المرجعي مع المتغير المحلي. وSetLengthالتالية تحرر المخزن وقد تعيد استخدامه، فانتهت كل خانة إلى تسمية المغلف الأخير، واشتقت الملفات متعددة المستلمين المفتاح الخطأ. الإصلاح تخزين نسخة مملوكة عبرList.Add(Pointer(System.Copy(Bytes))) - فك المغلفات يفك DER في موضعه، ومرور استعادة المفاتيح كان يبصم في الأصل تلك المصفوفات الحية نفسها. القارئ الآن يلتقط لقطات نقية من كل مغلف قبل أن يلمسها أي فك، وتجري البصمة على اللقطات
- DER الثنائي الممرر عبر
TStringListمن نوع Unicode تلقى بايتاته عند$80أو فوقها معاد ترميزها بصفحة الكود، لذا تخزن HotPDF المغلفات نصاً سداسياً داخلياً - يجب كتابة السلاسل المشفرة والثنائية سلاسل سداسية. السلسلة الحرفية تخضع لتوحيد نهايات الأسطر، حيث يصير CR و LF و CRLF كلها LF واحدة (§7.3.4.2 من ISO 32000-1)، وذلك يعيد كتابة النص المشفر بصمت. تطلق HotPDF كل مدخل
/Recipientsسلسلة سداسية وتعفيه من تشفير السلاسل، لأن كل قارئ يحتاج المغلفات قبل أن يملك أي مفتاح - أول بايت في
BIT STRINGمن DER يعد البتات غير المستخدمة ولا بد أن يكون صفراً للمفاتيح المتحاذية بالبايتات. تركه بلا تهيئة بعدSetLengthكتب ما كان على المكدس، وفاكّ صارم رفض مفتاح البادئ، فأمكن لملف أن يفشل أحياناً في الفتح بالمفتاح الذي كتب لأجله بالذات - حين لا يستطيع المفتاح نفسه فك التشفير بعد، قارن طبقة فوق طبقة: مفتاح الملف، ثم بادئة النص المشفر (IV)، ثم مفتاح الكائن، ثم النص الصريح. العيب يسكن مباشرة بعد أول طبقة تخالف
كيف تفتح PDF مشفراً بالشهادات بمفتاح خاص؟
لفتح ملف PDF مشفراً بالشهادات، سجل مادة المفتاح الخاص قبل استدعاء LoadFromFile، لأن HotPDF يستعيد مفتاح الملف أثناء المرور البنيوي. أسند مفتاح RSA أو EC مفكوكاً بـ HPDFParsePFX إلى PubSecKeyMaterial، وأضف مفاتيح RSA أخرى بـ AddPubSecKeyMaterial، وسجل أعداداً سلسلية ECDH خاماً بـ AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint)، باستخدام الثوابت HPDFOIDX25519 أو HPDFOIDX448 أو HPDFOIDECP256 أو HPDFOIDECP384 أو HPDFOIDECP521. منحنيات NIST تتطلب النقطة العامة غير المضغوطة الخاصة بالمستلم؛ ومنحنيات Montgomery تتجاهلها
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;
procedure OpenAuditPack(const LegalScalar: TBytes);
var
Reader: THotPDF;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
Reader.PubSecKeyMaterial :=
HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
// اختياري: اختر المغلف مباشرة بدل تجربتها كلها
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = جرّب كل مغلف بالترتيب
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
بلا نداء رجعي تجرب HotPDF كل مغلف مقابل كل مفتاح مسجل: المفتاح الأساسي أولاً، ثم كل مفتاح RSA إضافي، ثم مادة EC. يستلم PubSecRecipientQuery عدد المغلفات ويعيد فهارس صفرية الأساس أو 1-، والفهرس خارج المصفوفة يرفع استثناء بدل تقييده. انتبه أن AddPubSecKeyMaterial تقبل مادة RSA فقط (تصر على معامل ومأسس خاص)، فمفاتيح EC مكانها PubSecKeyMaterial أو AddPubSecAgreementKeyMaterial. وحين لا يفك أي مفتاح أي مغلف يعود مرور الاستعادة دون مفتاح ملف بدل أن يرفع، فتحقق أن المحتوى الذي تتوقعه فُك تشفيره فعلاً بدل الثقة بأن استدعاء التحميل عاد
ما لا تضمنه HotPDF
تضمن HotPDF أن كاتبها وقارئها يتفقان بايتاً ببايت، وتبني مغلفات تتبع بنى CMS المقتبسة أعلاه. وهي لا تضمن أن كل عارض PDF يفتح كل تركيبة. دعم نقل مفاتيح RSA-OAEP ودعم مستلمي X25519 أو X448 يتفاوت بين القراء والإصدارات، ولم ننشر نتائج توافق لتلك التركيبات. إن كان على مستند أن يفتح في عارض بعينه، شفر ملف اختبار لشهادة اختبار من نوع المفتاح نفسه وافتحه هناك قبل أن تلتزم بنمط. والصلاحيات المحمولة في المغلف تبقى سياسة يحترمها البرنامج المطابق، تماماً كما هي تحت تشفير كلمات السر. وجودة البذرة مسؤوليتك أنت أيضاً: AESGenerateRandomBytes موجودة لتلك المهمة، وتمسح HotPDF نسختها من البذرة بمجرد اشتقاق مفتاح الملف. وإن احتجت كذلك أن يستخدم سلسلة أو تدفقاً أو مرفقاً مرشح تشفير مختلفاً، يبين دليل سياسات مرشحات التشفير لـ StmF و StrF و EFF أسماء المرشحات التي يقبلها معالج المفتاح العام
تشفير الشهادات ومغلفات مستلمي RSA-OAEP و ECDH وتحميل المفتاح الخاص كلها تصدر في مكون PDF من HotPDF لـ Delphi، إلى جانب تشفير كلمات السر والتواقيع الرقمية وبقية صندوق أدوات ISO 32000 لـ Delphi و C++Builder