مقاله فنی

تلاش‌مجدد رمز عبور PDF رمزنگاری‌شده در Delphi با PDFlibPas

PDFlibPas یک رمز عبور اشتباه روی یک PDF رمزنگاری‌شده را با دورریختن TPDFDocumentای که همین الان شکست خورده و ساختن یک عدد کاملاً جدید برای تلاش بعدی، هدایت‌شده با یک callback به نام OnPassword (‏TPDFlibPasswordEvent) که تا شانزده تلاش پیش از تسلیم‌شدن اجرا می‌شود، تلاش‌مجدد می‌کند. این یک انحراف عمدی از غریزه‌ای است که اغلب توسعه‌دهندگان Delphi ابتدا به‌سراغش می‌روند: شیء سند که از پیش در حافظه نشسته را نگه دارید، یک رمز عبور تصحیح‌شده به آن بدهید، و دوباره درجا بارگذاری کنید به‌جای شروع دوباره از هیچ. حلقه‌ی تلاش‌مجدد PDFlibPas، افزوده‌شده در v3.245.0، موضع مخالف را می‌گیرد، به دلایلی مشخص برای آنچه یک تلاش رمز عبور شکست‌خورده پشت سر می‌گذارد. سناریوی پشت آن به‌اندازه‌ی کافی معمولی است که اغلب برنامه‌های سند-محور Delphi در نهایت به آن برمی‌خورند: یک صفحه‌ی دریافت یک PDF می‌پذیرد، یک trailer رمزنگاری‌شده یک دیالوگ رمز عبور را اجبار می‌کند، اپراتور رشته را اشتباه تایپ می‌کند، و دیالوگ برای یک تلاش دوم دوباره ظاهر می‌شود. هیچ چیزی درباره‌ی آن تجربه‌ی کاربری غیرمعمول نیست، پس کد پشتش باید بیش از یک رمز عبور کاندید برای همان فایل بپذیرد، و باید آن کار را امن انجام دهد، بدون نشت‌دادن وضعیت از تلاش رد‌شده به تلاشی که به‌دنبالش می‌آید

چرا نمی‌توانید فقط روی همان شیء سند تلاش‌مجدد کنید؟

دوباره‌استفاده‌کردن از یک TPDFDocument در سراسر تلاش‌های رمز عبور کار نمی‌کند، چون یک تلاش شکست‌خورده از پیش آن شیء را داخلاً باز کرده به‌جای اینکه آن را در یک وضعیت متوقف‌شده و قابل‌ازسرگیری رها کند. بازکردن یک PDF رمزنگاری‌شده یعنی تجزیه‌ی جدول cross-reference، ساخت یک خواننده روی منبع زیرین، و ساخت یک crypt handler از هر رمز عبوری که تأمین شده، همه پیش از اینکه PDFlibPas حتی بتواند بررسی کند آیا آن رمز عبور درست است. وقتی رمز عبور اشتباه درمی‌آید، روال بارگذاری داخلی سند خواننده، جدول cross-reference، و crypt handler را به‌عنوان بخشی از شکست‌خوردن پاک می‌کند، دقیقاً همان‌طور که باید، که یعنی هیچ تجزیه‌گر نیمه‌ساخته‌ای آنجا منتظر یک رمز عبور تصحیح‌شده در یک فراخوانی دوم نمی‌نشیند. همان شیء را در هر صورت از یک فراخوانی بارگذاری دیگر عبور دهید و حالت شکست دقیقاً همان نوعی است که برای دیباگ‌کردن بدبختی است: یک خطا از وضعیت داخلی ساخته‌شده برای یک تجزیه‌ی متفاوت و از پیش شکست‌خورده نمایان می‌شود، بدون هیچ چیزی که آشکارا به عقب به رمز عبور سه فراخوانی بالادست اشاره کند. PDFlibPas کل رده‌ی مسئله را با اینکه هرگز تلاش نکند یک شیء سند را به‌محض شکست‌خوردن در باز‌شدن بازیابی کند اجتناب می‌کند؛ هر تلاش سندی می‌گیرد که هرگز یک رمز عبور اشتباه ندیده، خواننده و جدول cross-reference شامل

callback به نام OnPassword چطور رمز عبور بعدی را درخواست می‌کند؟

TPDFlibPasswordEvent نوع callbackای است که PDFlibPas از طریق TPDFlib.LoadFromFile، LoadFromStream، و LoadFromString هر زمان که رمز عبور همین الان امتحان‌شده اشتباه درمی‌آید فرا می‌خواند، و سه چیز را به handler می‌سپارد: کدام تلاش در شرف اجراست، یک پارامتر 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 به‌عنوان ۲ می‌رسد. Retry را تنظیم‌نشده رها کنید و بارگذاری تمیز با LastErrorCode برابر 404 شکست می‌خورد؛ آن را true تنظیم کنید و PDFlibPas دوباره تلاش می‌کند با هر چیزی که handler همین الان درون Password نوشته

درون حلقه‌ی تلاش‌مجدد: یک TPDFDocument جدید به‌ازای هر تلاش

داخلاً، PDFlibPas سؤال چرخه‌ی عمر شیء را به همان روش برای LoadFromFile، LoadFromStream، و LoadFromString پاسخ می‌دهد: هر تلاش، اولی شامل، یک TPDFDocument تازه می‌سازد، آن را از طریق دنباله‌ی کامل بازکردن با هر رمز عبوری که آن تلاش استفاده می‌کند اجرا می‌کند، و فقط اگر رمز عبور تأیید شود شیء را نگه می‌دارد. TPDFDocument یک تلاش رد‌شده بلافاصله آزاد می‌شود، خواننده، جدول cross-reference، و crypt handلرش را با خودش پایین می‌کشد، و تلاش بعدی با شیئی که اصلاً هیچ تاریخچه‌ای ندارد از نو شروع می‌شود

// 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 برای هر سندی که فراخواننده باز دارد نگه می‌دارد، افزوده می‌شود. هیچ چیزی درباره‌ی یک تلاش رد‌شده از بیرون حلقه‌ی تلاش‌مجدد قابل‌مشاهده نیست: نه یک خواننده‌ی نیمه-مقداردهی‌شده، نه یک شمار صفحه‌ی کهنه، نه یک crypt handler ساخته‌شده از کلید اشتباه

PDFlibPas چند بار یک رمز عبور اشتباه را تلاش‌مجدد می‌کند؟

PDFlibPas شانزده تلاش کل را در برابر یک فراخوانی تکی LoadFromFile، LoadFromStream، یا LoadFromString مجاز می‌کند، با شمارش رمز عبوری که به خودِ فراخوانی پاس داده شده به‌عنوان تلاش یک. OnPassword فقط برای تلاش‌های دو تا شانزده شلیک می‌شود، که callback را به پانزده فراخوانی محدود می‌کند؛ یک تلاش هفدهم درخواست کنید و PDFlibPas بدون حتی فراخوانی handler امتناع می‌کند. Retry را در هر نقطه‌ای در پیش‌فرضش یعنی false رها کنید، یا هر شانزده تلاش را بدون یک رمز عبور درست خرج کنید، و LoadFromFile مقدار 0 را با LastErrorCode تنظیم‌شده روی 404، کد PDFlibPas برای یک رمز عبور رد‌شده، برمی‌گرداند. سقف به دلایلی فراتر از تمیزی وجود دارد: یک حلقه‌ی تلاش‌مجدد بدون‌حد یک راه آسان برای تبدیل یک رمز عبور اشتباه‌تایپ‌شده به یک انکار سرویس تصادفی در برابر هر رشته‌ای که بارگذاری را اجرا می‌کند است، به‌خصوص به‌محض اینکه یک handler به چیزی خودکار سیم‌کشی شود، مثل یک فهرست رمزهای عبور قبلاً دیده‌شده، به‌جای یک انسان که روی یک دیالوگ کلیک می‌کند. PDFlibPas همچنین Abort فراخوانی‌شده روی نمونه‌ی TPDFlib از درون handler را رعایت می‌کند، چون Sender به‌عنوان همان شیء می‌رسد، مفید پشت یک دکمه‌ی Cancel روی یک دیالوگ رمز عبور، و حلقه‌ی تلاش‌مجدد را در بررسی بعدی صرف‌نظر از اینکه Retry به چه چیزی تنظیم شده متوقف می‌کند. یک بارگذاری که به دلیلی جز یک رمز عبور اشتباه شکست بخورد، مثلاً یک جدول cross-reference آسیب‌دیده، اصلاً هرگز وارد حلقه‌ی تلاش‌مجدد نمی‌شود: PDFlibPas LastErrorCode برابر 401 گزارش می‌دهد و پس از اولین تلاش متوقف می‌شود، چون هیچ تعداد حدس رمز عبور یک فایل ساختاری خراب را رفع نمی‌کند

آیا حلقه‌ی تلاش‌مجدد برای فایل‌ها، جریان‌ها، و رشته‌ها یکسان کار می‌کند؟

callback به نام OnPassword و سقف شانزده-تلاشی در سراسر LoadFromFile، LoadFromStream، و LoadFromString یکسان رفتار می‌کنند، هرچند سه نقطه ورود منبعشان را بین تلاش‌ها متفاوت نگه می‌دارند. یک مسیر فایل ارزان برای بازدید دوباره است، چون هر تلاش صرفاً فایل نام‌داده‌شده را دوباره باز می‌کند، و یک منبع رشته‌ای از پیش به‌عنوان کپی خودِ فراخواننده در حافظه می‌نشیند، پس هیچ‌کدام به کمکی از فراخواننده بین تلاش‌ها نیاز ندارند. یک جریان تأمین‌شده‌ی-فراخواننده همان یک موردی است که ارزش مکث دارد: LoadFromStream آن جریان را به موقعیت صفر برمی‌گرداند و آن را داخلاً پیش از اولین تلاش تجزیه کپی می‌کند، پس هر تلاش بعدی، و TPDFDocument تازه‌ساخته‌شده پشتش، از آن کپی داخلی بازپخش می‌شود نه از هر جایی که یک تجزیه‌ی شکست‌خورده موقعیت جریان را رها کرده. PDFlibPas را یک TFileStream یا TMemoryStream برای یک سند محافظت‌شده‌با-رمزعبور بدهید و هیچ نیازی به rewind‌کردنش بین تلاش‌های مجدد نیست؛ PDFlibPas از پیش موقعیتی را که یک تلاش اول و شکست‌خورده ممکن است جابه‌جا کرده باشد در نظر می‌گیرد

جای‌دادن تلاش‌مجدد رمز عبور در یک صفحه‌ی دریافت سند

یک گردش‌کار دریافت سند خانه‌ی طبیعی این callback است، چون دقیقاً همان شکل مسئله‌ای است که 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 برگردانده باشد، به‌معنای اینکه یک رمز عبور در آن تبادل واقعاً در برابر crypt handler فایل تأیید شده؛ یک تلاش رد‌شده هرگز به آن خط نمی‌رسد، و نه یک سند نیمه‌باز. آنچه بعداً می‌آید، به‌محض اینکه یک سند مثل این تأیید-باز شود، ارزش یک نگاه دوم به تنظیمات محافظتش دارد به‌جای فرض‌کردن اینکه رمز عبوری که کار کرد کل داستان امنیت است: ممیزی آنچه دیکشنری /Encrypt یک سند واقعاً اعلان می‌کند خواندن الگوریتم، revision، و بیت‌های مجوز که PDFlibPas به‌محض اینکه یک فایل مثل این بارگذاری شود در معرض دید می‌گذارد را پوشش می‌دهد

تلاش‌مجدد رمز عبور همچنین نمونه‌ای محدود از انضباطی گسترده‌تر است که PDFlibPas در سراسر لایه‌ی تجزیه‌اش اعمال می‌کند: فایلی که هنوز خودش را ثابت نکرده هیچ فایده‌ای از تردید نمی‌گیرد، چه سؤال این باشد که کدام رمز عبور آن را باز می‌کند چه اینکه آیا یک فیلد طول درونش درباره‌ی اندازه‌ی بافری که نیاز دارد دروغ می‌گوید. تقویت یک تجزیه‌گر PDF در Pascal در برابر فایل‌های مخرب نیمه‌ی دیگر آن انضباط را پوشش می‌دهد، رمزگشاهایی که هر برنامه‌ی فونت و جریان تصویری در یک PDF ورودی را به‌عنوان ورودی خصمانه در نظر می‌گیرند نه یک سند درست‌شکل که صرفاً رمز عبورش را فراموش کرده

OnPassword و حلقه‌ی تلاش‌مجدد پشتش بخشی از کتابخانه‌ی استاندارد PDF از PDFlibPas برای Delphi و C++Builder هستند، در دسترس هرجایی که LoadFromFile، LoadFromStream، یا LoadFromString از پیش هستند، بدون هیچ ماژول جداگانه یا رده‌ی مجوز لازم برای سندی که فقط به یک حدس دوم برای رمز عبورش نیاز دارد