مقال تقني

إعادة محاولة كلمات مرور PDF مشفَّرة في Delphi عبر PDFlibPas

يعيد PDFlibPas محاولة كلمة مرور خاطئة على ملف PDF مشفَّر بالتخلص من TPDFDocument الذي فشل للتو وإنشاء واحد جديد كليًا للمحاولة التالية، مدفوعًا بدالة رد نداء OnPassword (TPDFlibPasswordEvent) تعمل حتى ست عشرة محاولة قبل الاستسلام. هذا خروج متعمد عن الغريزة التي يلجأ إليها معظم مطوري Delphi أولًا: أبقِ كائن المستند الجالس بالفعل في الذاكرة، وغذّه بكلمة مرور مصححة، وأعد التحميل في مكانه بدلًا من البدء من جديد من لا شيء. تتخذ حلقة إعادة المحاولة في PDFlibPas، المُضافة في v3.245.0، الموقف المعاكس، لأسباب خاصة بما تتركه خلفها محاولة كلمة مرور فاشلة. السيناريو خلف ذلك عادي بما يكفي بحيث تصطدم به معظم تطبيقات Delphi الكثيفة المستندات في نهاية المطاف: شاشة استلام تقبل ملف PDF، ويفرض ذيل مشفَّر مربع حوار كلمة مرور، ويخطئ المشغّل في كتابة السلسلة، ويظهر مربع الحوار مرة أخرى لمحاولة ثانية. لا شيء غريب في تجربة المستخدم تلك، بحيث يتعين على الشيفرة خلفها قبول أكثر من كلمة مرور مرشحة للملف نفسه، ويجب أن تفعل ذلك بأمان، دون تسريب حالة من المحاولة المرفوضة إلى تلك التي تليها

لماذا لا يمكنك ببساطة إعادة المحاولة على كائن المستند نفسه؟

لا تعمل إعادة استخدام TPDFDocument عبر محاولات كلمة المرور، لأن محاولة فاشلة قد هدمت بالفعل ذلك الكائن داخليًا بدلًا من تركه في حالة موقوفة قابلة للاستئناف. فتح ملف PDF مشفَّر يعني تحليل جدول المرجع المتقاطع، وبناء قارئ فوق المصدر الكامن، وبناء معالج تشفير من أيًّا كانت كلمة المرور المُقدَّمة، كل ذلك قبل أن يستطيع PDFlibPas حتى اختبار ما إذا كانت تلك كلمة المرور صحيحة. عندما تتضح خطأ كلمة المرور، ينظّف روتين التحميل الداخلي للمستند القارئ، وجدول المرجع المتقاطع، ومعالج التشفير كجزء من الفشل، تمامًا كما ينبغي، ما يعني عدم وجود مُحلِّل نصف مبني يجلس هناك ينتظر كلمة مرور مصححة في استدعاء ثانٍ. شغّل ذلك الكائن نفسه عبر محاولة تحميل أخرى رغم ذلك ونمط الفشل هو بالضبط النوع البائس لتصحيح الأخطاء: يظهر خطأ من حالة داخلية مبنية لتحليل مختلف فشل بالفعل، دون أي شيء فيه يشير بوضوح إلى كلمة المرور ثلاث استدعاءات في المنبع. يتجنب PDFlibPas فئة المشكلة بأكملها بعدم محاولة استعادة كائن مستند أبدًا بمجرد فشله في الفتح؛ تحصل كل محاولة على مستند لم يرَ قط كلمة مرور خاطئة، بما في ذلك القارئ وجدول المرجع المتقاطع

كيف تطلب دالة رد النداء OnPassword كلمة المرور التالية؟

TPDFlibPasswordEvent هو نوع دالة رد النداء التي يستدعيها PDFlibPas عبر TPDFlib.LoadFromFile وLoadFromStream وLoadFromString كلما تبين أن كلمة المرور التي جُرِّبت للتو خاطئة، وتسلّم للمعالج ثلاثة أشياء: أي محاولة على وشك التشغيل، ومعامل Password للكتابة فوقه بالمرشح التالي، وعلم Retry يكون افتراضيًا false

TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean) of object;

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

تُحتسب كلمة المرور المُمرَّرة في استدعاء LoadFromFile الأصلي كمحاولة واحدة، بحيث في أول مرة يُطلَق فيها OnPassword على الإطلاق، تصل AttemptNumber كـ2. اترك Retry غير مضبوطة ويفشل التحميل بنظافة بـLastErrorCode يساوي 404؛ اضبطها على true وسيحاول PDFlibPas مرة أخرى بأيًّا كان ما كتبه المعالج للتو في Password

داخل حلقة إعادة المحاولة: TPDFDocument جديد لكل محاولة

داخليًا، يجيب PDFlibPas عن سؤال دورة حياة الكائن بالطريقة نفسها لـLoadFromFile وLoadFromStream وLoadFromString: كل محاولة، بما في ذلك الأولى، تبني TPDFDocument جديدًا، وتشغّله عبر تسلسل الفتح الكامل بأيًّا كانت كلمة المرور التي تستخدمها تلك المحاولة، ولا تحتفظ بالكائن إلا إذا تحققت كلمة المرور. يُحرَّر TPDFDocument الخاص بمحاولة مرفوضة فورًا، آخذًا قارئه، وجدول مرجعه المتقاطع، ومعالج التشفير الخاص به معه، وتبدأ المحاولة التالية من جديد بكائن ليس له أي تاريخ على الإطلاق

// Simplified excerpt from inside LoadFromFile: every attempt gets a
// document that has never seen a previously rejected password. FileName,
// AttemptNumber and AttemptPassword come from the enclosing method.
Var
  Doc: TPDFDocument;
  LoadResult: TPLLoadResult;
  Success: Boolean;
Begin
  Success := False;
  Repeat
    Doc := TPDFDocument.Create;
    Doc.DecodeMode := FDefaultDecodeMode;
    Try
      LoadResult := Doc.LoadFromFile(FileName, AttemptPassword);
      Success := LoadResult = lrOkay;
      if Success then
      begin
        FDocs.Add(Doc);            // hand the verified document to the
        Doc := nil;                 // caller's collection; skip the Free below
      end;
    Finally
      Doc.Free;                     // a rejected attempt's reader, xref table
    End;                            // and crypt handler are torn down right here
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // success, or a non-password failure: stop
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

سطر Doc := nil ذاك مباشرة قبل كتلة Finally هو عقد دورة حياة الكائن بأكمله في جملة واحدة. مستند يفشل يحمل حالة مُحلِّله نصف المبنية إلى قبره معه، بالتصميم، ومستند ينجح هو الوحيد الذي يُضاف قط إلى FDocs، المجموعة التي تحتفظ بها TPDFlib لكل مستند فتحه المستدعي. لا شيء من محاولة مرفوضة مرئي من خارج حلقة إعادة المحاولة: لا قارئ نصف مُهيَّأ، ولا عدد صفحات قديم، ولا معالج تشفير مبني من المفتاح الخاطئ

كم مرة سيعيد PDFlibPas محاولة كلمة مرور خاطئة؟

يسمح PDFlibPas بست عشرة محاولة إجمالية مقابل استدعاء واحد لـLoadFromFile أو LoadFromStream أو LoadFromString، محتسبًا كلمة المرور المُمرَّرة في الاستدعاء نفسه كمحاولة واحدة. لا تُطلَق OnPassword إلا للمحاولات من الثانية حتى السادسة عشرة، ما يحدّد دالة رد النداء عند خمس عشرة استدعاءً؛ اطلب محاولة سابعة عشرة وسيرفض PDFlibPas دون حتى استدعاء المعالج. اترك Retry عند افتراضيها false في أي نقطة، أو استنفد كل المحاولات الست عشرة دون كلمة مرور صحيحة، وستُعيد LoadFromFile القيمة 0 مع ضبط LastErrorCode على 404، رمز PDFlibPas لكلمة مرور مرفوضة. يوجد الحد الأقصى لأسباب تتجاوز النظافة: حلقة إعادة محاولة غير محدودة طريقة سهلة لتحويل كلمة مرور واحدة أُخطئ في كتابتها إلى حرمان خدمة عرضي ضد أيًّا كان الخيط الذي يشغّل التحميل، خاصة بمجرد ربط معالج بشيء آلي، مثل قائمة كلمات مرور شوهدت سابقًا، بدلًا من إنسان ينقر عبر مربع حوار. يحترم PDFlibPas أيضًا Abort المُستدعاة على مثيل TPDFlib من داخل المعالج، بما أن Sender يصل كذلك الكائن نفسه، مفيد خلف زر إلغاء على مربع حوار كلمة مرور، ويوقف حلقة إعادة المحاولة عند الفحص التالي بصرف النظر عمّا ضُبطت عليه Retry. لا يدخل تحميل يفشل لسبب غير كلمة مرور خاطئة، جدول مرجع متقاطع تالف مثلًا، حلقة إعادة المحاولة على الإطلاق: يبلّغ PDFlibPas عن LastErrorCode يساوي 401 ويتوقف بعد المحاولة الأولى، لأنه لا عدد من تخمينات كلمة المرور يصلح ملفًا معطوبًا بنيويًا

هل تعمل حلقة إعادة المحاولة بالطريقة نفسها للملفات، والتدفقات، والسلاسل؟

تتصرف دالة رد النداء OnPassword وحد المحاولات الست عشرة بشكل متطابق عبر LoadFromFile وLoadFromStream وLoadFromString، رغم أن نقاط الدخول الثلاث تحتفظ بمصدرها بشكل مختلف بين المحاولات. مسار ملف رخيص إعادة زيارته، بما أن كل محاولة تعيد ببساطة فتح الملف المسمّى، ومصدر سلسلة نصية يجلس بالفعل في الذاكرة كنسخة المستدعي الخاصة، بحيث لا يحتاج أي منهما أي مساعدة من المستدعي بين المحاولات. تدفق مُقدَّم من المستدعي هو الحالة الوحيدة الجديرة بالتوقف عندها: تعيد LoadFromStream ذلك التدفق إلى الموضع صفر وتنسخه داخليًا قبل محاولة التحليل الأولى، بحيث تعيد كل محاولة لاحقة، وTPDFDocument المُنشأ حديثًا خلفها، التشغيل من تلك النسخة الداخلية بدلًا من أيًّا كان أين ترك تحليل فاشل موضع التدفق. سلّم PDFlibPas كائن TFileStream أو TMemoryStream لمستند محمي بكلمة مرور ولا حاجة لإعادته إلى البداية بين إعادة المحاولات؛ يحسب PDFlibPas بالفعل حساب موضع قد تكون محاولة أولى فاشلة قد حرّكته

إدماج إعادة محاولة كلمة المرور في شاشة استلام مستند

سير عمل استلام مستند هو الموطن الطبيعي لدالة رد النداء هذه، لأنها بالضبط شكل المشكلة التي بُنيت OnPassword لحلها: يصل ملف من خارج التطبيق، وكلمة مروره غير معروفة بيقين مسبقًا، ويحتاج الشخص الذي يقدم المرشحين أكثر من تخمين واحد دون أن تكتب الشيفرة المحيطة حلقة إعادة محاولة خاصة بها حول LoadFromFile

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber counts from 2: the password already tried was attempt 1.
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // Retry is False when the operator cancels, which leaves
  // LastErrorCode at 404 for the caller to report.
end;
procedure TIntakeForm.LoadInboundDocument;
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.OnPassword := SupplyPassword;
    if Lib.LoadFromFile('inbound-invoice.pdf', '') = 1 then
      RegisterIntakeDocument(Lib)        // only a verified document reaches here
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

لا تتلقى RegisterIntakeDocument Lib أبدًا إلا بمجرد أن تُعيد LoadFromFile القيمة 1، بمعنى أن كلمة مرور ما في ذلك التبادل تحققت فعليًا مقابل معالج التشفير الخاص بالملف؛ محاولة مرفوضة لا تصل أبدًا إلى ذلك السطر، ولا مستند نصف مفتوح أيضًا. ما يأتي بعد ذلك، بمجرد تأكيد فتح مستند كهذا، يستحق نظرة ثانية إلى إعدادات حمايته بدلًا من افتراض أن كلمة المرور التي عملت هي القصة الأمنية بأكملها: تغطي تدقيق ما يعلنه قاموس /Encrypt الخاص بمستند فعليًا قراءة الخوارزمية، والمراجعة، وبتات الإذن التي يعرضها PDFlibPas بمجرد تحميل ملف كهذا

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

OnPassword وحلقة إعادة المحاولة خلفها جزء من مكتبة PDFlibPas لـPDF لـDelphi وC++Builder القياسية، متاحة أينما كانت LoadFromFile أو LoadFromStream أو LoadFromString موجودة بالفعل، دون وحدة منفصلة أو مستوى ترخيص مطلوب لمستند يحتاج فقط تخمينًا ثانيًا لكلمة مروره