مقال تقني

لا يمكنك ترقيع ملف PDF مشفَّر بصمت في Delphi

خذ ملف PDF لفاتورة يحمل بالفعل تشفير AES-256 واطلب من مكوّن PDFium لـDelphi وC++Builder (PDFiumPas) ختمها بـPDF/A لأغراض الاحتفاظ الأرشيفي، أو توقيعها بـPAdES، عبر تحديث تزايدي بدلًا من إعادة كتابة كاملة. لن تصل المكتبة إلى ذلك بترقيع البايتات المشفَّرة مباشرة: تكتشف حاقنات علامات التوافق الست الخاصة بها إدخال /Encrypt موجودًا وتُمرّر المصدر إلى الوجهة بايتًا بايت دون تغيير، ويُطلق موقّع PAdES الخاص بها استثناءً بدلًا من إصدار توقيع لن يقبله أي مُتحقق

هذا سؤال مختلف عن تدقيق ملف PDF لم تنشئه بحثًا عن مخاطر خفية، وهو تمرين للقراءة فقط بحد ذاته. هذه المقالة عن جانب الكتابة من حد الثقة نفسه: ما الذي يُسمح لشيفرتك الخاصة بفعله بملف بايتاته مقفلة بالفعل خلف كلمة مرور شخص آخر، في اللحظة التي تحاول فيها تلك الشيفرة إضافة أي شيء إليه لاحقًا

ما الذي يتطلبه ISO 32000-1 عند تحديث ملف PDF مشفَّر؟

يتطلب ISO 32000-1 §7.5.6 أن يكرر ذيل تحديث تزايدي كل إدخال من الذيل السابق باستثناء /Prev، ويسرد الجدول 15 /Encrypt ضمن الإدخالات التي يمكن لذيل حملها. أسقطه من الذيل الجديد ولن يملك قارئ متوافق سببًا للشك في الإغفال: الذيل الأحدث موثوق، بحيث يقرر قارئ لا يجد فيه /Encrypt أن الملف بأكمله غير مشفَّر ويحاول تحليل الجسم الأقدم لا يزال مُشفّرًا كبايتات عادية. أبقِ /Encrypt في الذيل الجديد لكن اكتب كائنات التحديث نفسها كنص صريح، وينتقل الفشل خطوة واحدة لاحقًا فقط: يكتشف القارئ التشفير بشكل صحيح، ويُشغّل كل كائن يلمسه عبر شيفرة الملف، بما في ذلك الكائنات الجديدة التي لم تُشفَّر قط أصلًا، ويسترجع ضوضاء لمحتوى كان مقروءًا تمامًا قبل أن يلمسه فك التشفير. أي من الخطأين ينتج ملفًا يبدو كتحديث تزايدي عادي سليم البنية على مستوى البايت، إلى أن يفتحه قارئ متوافق

ست حاقنات علامات، بوابة تشفير واحدة v2.14.2

يشحن PDFiumPas ست حاقنات علامات على مستوى البايت، واحدة لكل مجموعة فرعية من معيار PDF الدولي يمكنه تسميتها: PDF/A (ISO 19005)، وPDF/X (ISO 15930)، وPDF/UA (ISO 14289-1)، وPDF/E-1 (ISO 24517-1)، وPDF/R-1 (ISO 23504-1)، وPDF/VT-1 (ISO 16612-2). تأخذ كل واحدة البايتات التي كتبتها بالفعل FPDF_SaveAsCopy الخاصة بـPDFium نفسها وتضع فوقها تحديثًا تزايديًا ثانيًا أصغر: تدفق بيانات XMP وصفية جديد، وتعديل قاموس دليل يشير إليه، وبالنسبة للمجموعات الفرعية الموجَّهة للطباعة نية إخراج (OutputIntent) وملف تعريف ICC. اعتبارًا من v2.14.2، تقرأ كل واحدة من InjectPdfAMarkers وInjectPdfXMarkers وInjectPdfUaMarkers وInjectPdfEMarkers وInjectPdfRMarkers وInjectPdfVTMarkers ذيل المصدر أولًا، وإذا أبلغت عن إدخال /Encrypt موجود، تنسخ المصدر إلى تدفق الوجهة دون تعديل وتعود فورًا. لا XMP، ولا OutputIntent، ولا تعديل دليل — يحصل المستدعي على الملف الأصلي مرة أخرى، بايتًا بايت

var
  Src, Dst: TFileStream;
  Opts: TPdfXSaveOptions;
begin
  Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
  Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
  try
    Opts.Conformance := pxc4;
    InjectPdfXMarkers(Src, Dst, Opts);
    // pdfx-attempt.pdf is byte-identical to the source: still encrypted,
    // no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
    // nothing was corrupted either
  finally
    Dst.Free;
    Src.Free;
  end;
end;

مسموح بالتشفير ليس مثل آمن للحقن فيه

يسمح كل من PDF/E-1 وPDF/R-1 صراحة بأن يكون مستنده المضيف مشفَّرًا على مستوى المعيار، ما يُقرأ كإعفاء حتى تنظر إلى ما يجب أن يحدث فعليًا على القرص. يسمح ISO 24517-1 §6.3 بالتشفير لـPDF/E-1، ويسمح ISO 23504-1 §6.2.3 به لـPDF/R-1 شريطة أن تُعلن الترويسة %PDF-2.0. لا يقول أي من البندين أي شيء عمّا إذا كان بإمكان معالج لاحق على مستوى البايت إضافة كائن نص صريح بأمان إلى تلك الحاوية المشفَّرة بأمان، ولا يمكنه ذلك، للأسباب نفسها في §7.5.6 التي تنطبق على كل مجموعة فرعية أخرى. تسجّل مُتحققا التوافق الخاصان بـPDFiumPas لهذين الملفين الشخصيين، ValidatePdfECompliance وValidatePdfRCompliance، وجود /Encrypt عمدًا دون تعليمه كعيب، وهو صحيح لمُتحقق للقراءة فقط لا يكتب بايتًا أبدًا. إنه أيضًا نمط سهل التصفح السريع وافتراض أن الحاقن الشقيق لا يحتاج حارسًا منفصلًا، بينما الحاقن هو الدالة الوحيدة في الزوج التي يتعين عليها فعليًا الرفض

هل تُلغي SaveAsPdfX تشفير مستندك بصمت؟

نعم، كلما مررت عبر طرائق الراحة العامة بدلًا من استدعاء حاقن مباشرة. يرسم كل من TPdf.SaveAsPdfA وSaveAsPdfX وSaveAsPdfUa وSaveAsPdfE وSaveAsPdfR وSaveAsPdfVT المستند الحالي إلى تدفق مؤقت بـSaveAs(Tmp, saRemoveSecurity) قبل تسليم تلك البايتات إلى الحاقن المطابق. يُطابق saRemoveSecurity علم FPDF_REMOVE_SECURITY الخاص بـPDFium نفسه، بحيث لم تُشفَّر النسخة المؤقتة التي يستقبلها الحاقن قط أصلًا، ولا يملك حارس /Encrypt الخاص بالحاقن سببًا للتفعيل أبدًا. تحمل المخرجات علامات PDF/A، أو PDF/X، أو PDF/UA، أو PDF/E-1، أو PDF/R-1، أو PDF/VT-1 الخاصة بك، لكنها لم تعد محمية بأيًّا كانت كلمة المرور التي فتحت المصدر

تلك المقايضة غير مرئية حتى يفتح شخص لاحق النسخة الأرشيفية "المحمية" دون كلمة مرور ويلاحظ أنها تعمل ببساطة. الإصلاح ليس استدعاء طريقة مختلفة؛ لا يملك PDFiumPas نظير saAddSecurity يقترن بـsaRemoveSecurity، لأن محرك PDFium الكامن لم يُبنَ قط لكتابة تشفير جديد، فقط لإزالته. إذا كانت كلتا الخاصيتين مهمتين لملف واحد، يجب أن يكون التشفير خطوة منفصلة تمتلكها أنت، مُطبَّقة بعد علامات التوافق، لا مطوية في استدعاء SaveAsPdfA نفسه

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';           // needed to open the source at all
    Pdf.FileName := 'signed-encrypted-invoice.pdf';
    Pdf.LoadDocument;
    Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
    // invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
    // ran first inside SaveAsPdfA: the output opens without a password
  finally
    Pdf.Free;
  end;
end;

ماذا يحدث عند توقيع ملف PDF مشفَّر بـPAdES؟

يرفض PDFiumPas كليًا، بدلًا من إسقاط الطلب بصمت بالطريقة التي يفعلها حاقن العلامات. يمر كل من TPdf.SignPades وSignPadesToStream عبر SignPadesBytes داخلية، وأول ما تفعله بعد تحليل ذيل المصدر هو التحقق من /Encrypt. إذا كان الإدخال موجودًا، تُطلق EPadesCrypto بالرسالة "SignPadesBytes: the source document is encrypted; remove encryption before signing" بدلًا من المتابعة أبعد من ذلك. تطبّق InjectPadesDssMarkers، الدالة التي تُضمِّن الشهادات، واستجابات OCSP، وقوائم إبطال الشهادات (CRL) للتحقق طويل الأمد، الفحص نفسه بالضبط للسبب نفسه بالضبط، برسالتها الخاصة: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

المنطق هنا أكثر صرامة من التمرير المباشر لحاقنات العلامات، وذلك عمدًا. التمرير المباشر الصامت آمن لختم PDF/A لأن تخطيه يتركك بملف PDF صالح نفسه الذي بدأت به، فقط دون تسمية. لا يمكن للتوقيع أن يفشل بهذا الصمت: توقيع لم يُضَف قط بصمت يبدو، لأي شيفرة مستدعية تتحقق فقط من نتيجة منطقية ثنائية، تمامًا كتوقيع أُضيف بنجاح. تنحدر EPadesCrypto من فئة Exception العادية، بحيث يكون التقاطها معالجة استثناءات عادية، لا اصطلاح تدفق تحكم خاص يجب عليك تعلّمه

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';
    Pdf.FileName := 'encrypted-contract.pdf';
    Pdf.LoadDocument;
    try
      Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
    except
      on E: EPadesCrypto do
        // E.Message: 'SignPadesBytes: the source document is encrypted;
        // remove encryption before signing'
        raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
    end;
  finally
    Pdf.Free;
  end;
end;

ترتيب أختام التوافق، والتوقيعات، والتشفير

الإصلاح العملي هو الترتيب، لا مكتبة مختلفة. طبّق علامات PDF/A، أو PDF/X، أو PDF/UA، أو PDF/E-1، أو PDF/R-1، أو PDF/VT-1 أولًا، وأضف أي توقيع PAdES بعد ذلك، ولا تُشغّل إلا حينها أيًّا كانت الخطوة في خط أنابيبك التي تمتلك التشفير فعليًا، سواء كانت كاتب PDF مخصصًا، أو جهاز توقيع، أو تنفيذ AES خاصًا بك. تلائم طبقة التحديث التزايدي في PDFiumPas بشكل طبيعي منتصف ذلك التسلسل، مُلحقة كائنات صغيرة ومستهدفة على ملف منتهٍ من ناحية أخرى، وينتمي التشفير إلى النهاية بالضبط لأنه العملية الوحيدة في السلسلة التي لا يستطيع PDFiumPas نفسه تنفيذها أو عكسها

لا شيء من هذا يغيّر كيفية قراءة PDFiumPas لبيانات الذيل والمرجع المتقاطع التي يعتمد عليها كل تحديث تزايدي، وهو مصدر دقة بحد ذاته بمجرد أن تدخل تدفقات xref الصورة؛ تغطي التحقق من تدفقات الكائنات وxref لملف PDF كيف يعالج مسار قراءة الذيل نفسه بنى PDF 1.5+ المضغوطة. وبمجرد أن يصبح مستند جاهزًا لشيء أقوى من ختم توافق، فإن توقيع ملف PDF بتوقيع PAdES B-B في Delphi هو حيث تتولى SignPades الأمر من النقطة نفسها التي تتركها هذه المقالة

تُشحن حاقنات العلامات وطرائق SignPades الموصوفة هنا كجزء من مكوّن PDFium لـDelphi وC++Builder، إلى جانب الرسم والفحص للقراءة فقط الذي يوفره PDFium أصلًا