مقاله فنی

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

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

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

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

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

TPDFlibPasswordEvent نوع callbackای است که PDF Library for Delphi از طریق 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 تنظیم کنید و PDF Library for Delphi دوباره تلاش می‌کند با هر چیزی که handler همین الان درون Password نوشته

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

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

فلوچارت PDF Library for Delphi از حلقه تلاش مجدد گذرواژه: هر تلاش یک TPDFDocument تازه می‌سازد، تلاش ردشده reader، جدول ارجاع متقابل و handler رمز آن را آزاد می‌کند، و callback با نام OnPassword تصمیم می‌گیرد که تلاش دیگری اجرا شود یا بارگذاری با LastErrorCode 404 شکست بخورد
نامزدِ شکست‌خورده هرگز از حلقه جان سالم به در نمی‌برد: سندش با وضعیت پارسرِ نیمه‌ساخته آزاد می‌شود. تنظیم Retry نامزد بعدی را در Password می‌نویسد و با شیءای تکرار می‌کند که هرگز گذرواژه نادرستی ندیده است
// گزیده‌ی ساده‌شده از درون LoadFromFile: هر تلاش یک
// سند می‌گیرد که هرگز یک رمز عبور رد‌شده‌ی قبلی را ندیده. FileName،
// AttemptNumber و AttemptPassword از متد فراگیرنده می‌آیند.
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);            // سند تأییدشده را به
        Doc := nil;                 // مجموعه‌ی فراخواننده تحویل بده؛ از Free زیر صرف‌نظر کن
      end;
    Finally
      Doc.Free;                     // خواننده، جدول xref یک تلاش رد‌شده
    End;                            // و crypt handler دقیقاً همین‌جا برچیده می‌شوند
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // موفقیت، یا شکستی غیرمرتبط با رمز عبور: توقف
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

آن خط Doc := nil دقیقاً پیش از بلوک Finally کل قرارداد چرخه‌ی عمر شیء در یک عبارت است. سندی که شکست می‌خورد وضعیت تجزیه‌گر نیمه‌ساخته‌اش را با خودش به گور می‌برد، از نظر طراحی، و سندی که موفق می‌شود تنها سندی است که هرگز به FDocs، مجموعه‌ای که TPDFlib برای هر سندی که فراخواننده باز دارد نگه می‌دارد، افزوده می‌شود. هیچ چیزی درباره‌ی یک تلاش رد‌شده از بیرون حلقه‌ی تلاش‌مجدد قابل‌مشاهده نیست: نه یک خواننده‌ی نیمه-مقداردهی‌شده، نه یک شمار صفحه‌ی کهنه، نه یک crypt handler ساخته‌شده از کلید اشتباه

PDF Library for Delphi چند بار یک رمز عبور اشتباه را تلاش‌مجدد می‌کند؟

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

PDF Library for Delphi: مسیر تلاش‌ها از یک تا شانزده که فعال شدن OnPassword از تلاش دوم به بعد، رد شدن تلاش هفدهم بدون فراخوانی handler، و پیامدهای جداگانه موفقیت، LastErrorCode 404 و LastErrorCode 401 را نشان می‌دهد
فقط گذرواژه‌ای که مستقیماً به فراخوانی load داده می‌شود تلاش اول محسوب می‌شود و callback هرگز بیش از پانزده بار در هر load اجرا نمی‌شود. فایلِ از نظر ساختاری معیوب از حلقه به‌طور کامل عبور می‌کند و پس از یک تلاش 401 گزارش می‌دهد

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

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

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

یک گردش‌کار دریافت سند خانه‌ی طبیعی این callback است، چون دقیقاً همان شکل مسئله‌ای است که OnPassword برای حل آن ساخته شده: یک فایل از بیرون برنامه می‌رسد، رمز عبورش از پیش با قطعیت شناخته نیست، و کسی که کاندیدها را تأمین می‌کند به بیش از یک حدس بدون اینکه کد پیرامون حلقه‌ی تلاش‌مجدد خودش را دور LoadFromFile بنویسد نیاز دارد

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber از 2 شمارش می‌شود: رمز عبوری که پیش‌تر امتحان شده تلاش 1 بوده است.
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // وقتی اپراتور لغو می‌کند Retry برابر False است، که
  // LastErrorCode را روی 404 برای گزارش فراخواننده باقی می‌گذارد.
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)        // فقط یک سند تأییدشده به اینجا می‌رسد
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument فقط زمانی Lib را دریافت می‌کند که LoadFromFile مقدار 1 برگردانده باشد، به‌معنای اینکه یک رمز عبور در آن تبادل واقعاً در برابر crypt handler فایل تأیید شده؛ یک تلاش رد‌شده هرگز به آن خط نمی‌رسد، و نه یک سند نیمه‌باز. آنچه بعداً می‌آید، به‌محض اینکه یک سند مثل این تأیید-باز شود، ارزش یک نگاه دوم به تنظیمات محافظتش دارد به‌جای فرض‌کردن اینکه رمز عبوری که کار کرد کل داستان امنیت است: ممیزی آنچه دیکشنری /Encrypt یک سند واقعاً اعلان می‌کند خواندن الگوریتم، revision، و بیت‌های مجوز که PDF Library for Delphi به‌محض اینکه یک فایل مثل این بارگذاری شود در معرض دید می‌گذارد را پوشش می‌دهد

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

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