مقال تقني

تشفير PDF بالشهادات في Delphi: RSA-OAEP و ECDH

يشفر 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 بكلمات السر وأعلام الصلاحيات الوجه الآخر من تلك المقايضة

مخطط تشفير المفتاح العام في HotPDF: تثبت EnablePubKeyEncryption بذرة من 20 بايت، ويشفر كل مغلف CMS من نوع EnvelopedData تلك البايتات العشرين زائد كلمة صلاحيات واحدة من 32 بت داخل /Filter /Adobe.PubSec بـ /SubFilter /adbe.pkcs7.s5 و /CFM /AESV3، ويفك القارئ مغلفاً واحداً ويستعيد البذرة ويبصمها مع كل مدخل /Recipients بترتيب المصفوفة لإعادة بناء مفتاح الملف
قيمة /P في قاموس التشفير مجرد عنصر نائب لأن الصلاحيات الحقيقية تسافر داخل كل مغلف، ولا يجوز لأي شيء في اتجاه المصب إعادة ترتيب المصفوفة التي تجري عليها البصمة أو إعادة ترميزها

كتابة مستلمي 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 من نقطة المستلم مقابل معادلة المنحنى، لكنها لا تستطيع فحص أن مفتاحك العام للبادئ يعود فعلاً إلى عدك السلسلي. النصفان غير المتطابقان ما زالا ينتجان مغلفاً سليم التكوين تماماً لا يستطيع أي مستلم فتحه، ولهذا تنتمي جولة تحميل ذهاباً وإياباً إلى مجموعة اختبارك، لا مجرد فحص حجم ملف

مخطط اتفاق ECDH في HotPDF: تشتق AddPubKeyAgreementRecipientWithSecret السر المشترك بكود منحنيات Pascal صافٍ، وتمزج UKM جديداً من 32 بايت عبر KDF من نوع stdDH بـ SHA-256 لـ P-256 و X25519 و SHA-384 لـ P-384 و SHA-512 لـ P-521 و X448، ثم تلف مفتاح المحتوى بلف مفتاح AES-256 من RFC 3394 لبناء مغلف KeyAgreeRecipientInfo
قيمة النمط من pkasECDHP256 إلى pkasX448 يجب أن تطابق مفتاح الشهادة، والنصفان غير المتطابقان من scalar والنقطة العامة ما زالا ينتجان مغلفاً سليم التكوين لا يفتحه أي مستلم

لماذا يهم ترتيب /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: تحمل PubSecKeyMaterial مفتاح RSA أو EC الأساسي من HPDFParsePFX، وتضيف AddPubSecKeyMaterial مفاتيح RSA فقط، وتسجل AddPubSecAgreementKeyMaterial أعداداً سلسلية ECDH خاماً تحت معرفات منحنيات HPDFOIDX25519 إلى HPDFOIDP521، وعند LoadFromFile يجرب المزود المفتاح الأساسي ثم كل مفتاح RSA إضافي ثم مادة EC مقابل كل مغلف
حين لا يفك أي مفتاح أي مغلف يعود مرور الاستعادة دون مفتاح ملف بدل أن يرفع، فتحقق أن المحتوى فُك تشفيره فعلاً أو ثبّت المغلف عبر PubSecRecipientQuery

ما لا تضمنه HotPDF

تضمن HotPDF أن كاتبها وقارئها يتفقان بايتاً ببايت، وتبني مغلفات تتبع بنى CMS المقتبسة أعلاه. وهي لا تضمن أن كل عارض PDF يفتح كل تركيبة. دعم نقل مفاتيح RSA-OAEP ودعم مستلمي X25519 أو X448 يتفاوت بين القراء والإصدارات، ولم ننشر نتائج توافق لتلك التركيبات. إن كان على مستند أن يفتح في عارض بعينه، شفر ملف اختبار لشهادة اختبار من نوع المفتاح نفسه وافتحه هناك قبل أن تلتزم بنمط. والصلاحيات المحمولة في المغلف تبقى سياسة يحترمها البرنامج المطابق، تماماً كما هي تحت تشفير كلمات السر. وجودة البذرة مسؤوليتك أنت أيضاً: ‏AESGenerateRandomBytes موجودة لتلك المهمة، وتمسح HotPDF نسختها من البذرة بمجرد اشتقاق مفتاح الملف. وإن احتجت كذلك أن يستخدم سلسلة أو تدفقاً أو مرفقاً مرشح تشفير مختلفاً، يبين دليل سياسات مرشحات التشفير لـ StmF و StrF و EFF أسماء المرشحات التي يقبلها معالج المفتاح العام

تشفير الشهادات ومغلفات مستلمي RSA-OAEP و ECDH وتحميل المفتاح الخاص كلها تصدر في مكون PDF من HotPDF لـ Delphi، إلى جانب تشفير كلمات السر والتواقيع الرقمية وبقية صندوق أدوات ISO 32000 لـ Delphi و C++Builder