مقاله فنی

رمزعبورهای SASLprep AES-256 در PDF با PDFlibPas در Delphi

یک PDF رمزنگاری‌شده با AES-256 که رمزعبور غیر-ASCII دارد، در برنامه‌ای که آن را نوشته باز می‌شود و هیچ‌جای دیگر. علت تقریباً همیشه یک مرحله آماده‌سازی گمشده است: ISO 32000-2 §7.6.4.3.3 لازم می‌داند رمزعبور با پروفایل SASLprep از stringprep پردازش شود پیش از آنکه UTF-8 کد شود و هش گردد. PDFlibPas، کتابخانه PDF برای Delphi و C++Builder، آن آماده‌سازی را داخل Encrypt، EncryptFile و DecryptFile انجام می‌دهد

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

چرا یک رمزعبور غیر-ASCII در یک reader باز می‌شود اما در دیگری نه؟

چون این دو برنامه توالی بایت متفاوتی از همان کلیدفشارها هش می‌کنند. اشتقاق کلید revision 6 در ISO 32000-2 §7.6.4.3.3 رمزعبور را به‌صورت بایت‌های UTF-8 می‌گیرد، تا ۱۲۷ بایت کوتاه می‌کند، یک salt اضافه می‌کند، و هش سخت‌شده را اجرا می‌کند؛ نتیجه در برابر entryهای /U و /O در دیکشنری رمزنگاری بررسی می‌شود. هیچ‌چیز در آن زنجیره مبهم نیست. یک بایت متفاوت در هرجای ورودی یک digest کاملاً متفاوت تولید می‌کند، اعتبارسنجی شکست می‌خورد، و reader دقیقاً یک چیز می‌تواند بگوید: رمزعبور اشتباه

بایت‌ها واگرا می‌شوند چون Unicode چند راه برای تایپ چیزی که به‌نظر همان رمزعبور می‌رسد ارائه می‌دهد. یک رمزعبور چینی ممکن است به‌صورت کاراکترهای precomposed از یک روش ورودی و به‌صورت فرم‌های سازگاری از دیگری برسد. یک رمزعبور آلمانی یا فرانسوی که از یک پردازشگر متن کپی شده می‌تواند یک NO-BREAK SPACE (U+00A0) حمل کند جایی که کاربر معتقد است یک فاصله معمولی است، یا یک SOFT HYPHEN (U+00AD) که به‌عنوان هیچ‌چیز رندر می‌شود. SASLprep وجود دارد تا همه این‌ها را به یک فرم کانونیک تبدیل کند پیش از آنکه هرکسی چیزی را هش کند، تا هر پیاده‌سازی مطابق همان کلید را از همان قصد مشتق کند

SASLprep واقعاً چه چیزی را درباره یک رمزعبور تغییر می‌دهد؟

RFC 4013 عبارت SASLprep را به‌عنوان پروفایلی از چارچوب stringprep در RFC 3454 تعریف می‌کند، و آن چهار مرحله ترتیبی است نه یک تبدیل واحد. نگاشت (Mapping) اول می‌آید: جدول C.1.2 (فاصله‌های غیر-ASCII) در RFC 3454 به U+0020 نگاشت می‌شود، و جدول B.1 (کاراکترهایی که معمولاً به هیچ نگاشت می‌شوند) کاملاً حذف می‌شود. سپس نرمال‌سازی به Unicode NFKC می‌آید، که مرحله‌ای است که کاراکترهای سازگاری و توالی‌های ترکیبی را تا می‌کند. سپس بررسی خروجی ممنوع هرچیز در جداول C.2.1 تا C.9 را رد می‌کند. در نهایت قانون دوجهته از بخش ۶ RFC 3454 روی رشته نرمال‌شده اعمال می‌شود

PDFlibPas کل پروفایل را در واحد PDFlibSASLprep پیاده‌سازی می‌کند، که یک نقطه ورود واحد ارائه می‌دهد. PLSASLprepPassword رمزعبور خام را می‌گیرد، فرم آماده‌شده را در یک پارامتر var می‌نویسد، و وقتی رمزعبور باید رد شود False برمی‌گرداند. این تابع عمداً روی مسیر خوشحال کامل است: یک رمزعبور صرفاً-ASCII بایت‌به‌بایت یکسان برمی‌گردد، بنابراین هیچ‌چیز درباره استقرارهای موجود تغییر نمی‌کند

uses
  PDFlibSASLprep;

var
  Prepared: WideString;
begin
  // RFC 4013: mapping, then NFKC, then prohibited output, then the bidi rule
  PLSASLprepPassword('I' + WideChar($00AD) + 'X', Prepared);  // -> 'IX'   B.1 deletes SOFT HYPHEN
  PLSASLprepPassword('a' + WideChar($00A0) + 'b', Prepared);  // -> 'a b'  C.1.2 maps NBSP to U+0020
  PLSASLprepPassword(WideString(WideChar($00AA)), Prepared);  // -> 'a'    NFKC folds ORDINAL INDICATOR
  PLSASLprepPassword(WideString(WideChar($2168)), Prepared);  // -> 'IX'   NFKC folds ROMAN NUMERAL NINE
  PLSASLprepPassword('user', Prepared);                       // -> 'user' ASCII is never touched
end;

ابهام U+200B که جداول حل نمی‌کنند

یک code point همزمان در دو جدول RFC 3454 می‌افتد، و دو جدول توافق ندارند. ZERO WIDTH SPACE (U+200B) داخل بازه C.1.2 از U+2000 تا U+200B می‌افتد، جایی که قانون می‌گوید آن را به U+0020 نگاشت کن، و همچنین داخل بازه B.1 از U+200B تا U+200D می‌افتد، جایی که قانون می‌گوید آن را حذف کن. مرحله نگاشت را به هر ترتیبی بخوانید بایت‌های متفاوتی از همان رمزعبور بیرون می‌آید: a+U+200B+b تحت C.1.2 به a b و تحت B.1 به ab آماده می‌شود. RFC 4013 هر دو جدول را نام می‌برد و نمی‌گوید کدام برنده است، پس این یک ابهام واقعی در مشخصات است نه یک خطای خواندن. PDFlibPas ابتدا عضویت C.1.2 را آزمایش می‌کند و بنابراین U+200B را به یک فاصله نگاشت می‌کند، که رفتاری است که سایر پیاده‌سازی‌های stringprep پرکاربرد روی آن مستقر شده‌اند؛ تطبیق با آن‌ها تنها چیزی است که اینجا اهمیت دارد، چون هدف توافق بایتی با هر readerی است که مشتری استفاده می‌کند

خواندن فایل‌های قدیمی: ابتدا آماده‌شده، دوم خام

راه‌حل مشکل سازگاری خودش را ایجاد می‌کند. هر فایل AES-256 که پیش از این تغییر نوشته شده رمزعبور UTF-8 خام را هش کرده، بنابراین ساختن reader به‌صورت کاملاً مطابق مشتری‌ها را از آرشیو خودشان بیرون می‌اندازد. PDFlibPas این را در سمت خواندن با امتحان دو نامزد به ترتیب حل می‌کند. TPDFDocument.SetPassword فهرست نامزدی می‌سازد که با فرم آماده‌شده شروع می‌شود و به فرم خام برمی‌گردد، و entry آماده‌شده را فقط زمانی اضافه می‌کند که سند واقعاً AES-256 باشد و دو فرم متفاوت باشند. برای یک رمزعبور ASCII فرم‌ها یکسانند، فهرست یک entry دارد، و هزینه کل مکانیزم یک مقایسه رشته واحد است. DecryptFile همان کار را در امتداد مسیر بازنویسی مستقیم AES-256 خودش انجام می‌دهد، با فراخوانی PLDirectDecryptFileAES256 با رمزعبور آماده‌شده اول

var
  Lib: TPDFlib;
  Bytes: AnsiString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.DrawText(100, 100, 'saslprep roundtrip');
    // Strength 3 and 4 are the two AES-256 values; both are prepared before hashing
    Lib.Encrypt('ow' + WideChar($00AD) + 'ner', 'pa' + WideChar($00AD) + 'ss', 4,
      Lib.EncodePermissions(1, 0, 0, 0, 0, 0, 0, 1));
    Bytes := Lib.SaveToString;
  finally
    Lib.Free;
  end;

  Lib := TPDFlib.Create;
  try
    // 'pass' is what SASLprep produced and what any conforming reader computes,
    // so the plain ASCII form opens a file created with the soft-hyphen form
    if Lib.LoadFromString(Bytes, 'pass') = 1 then
      Caption := IntToStr(Lib.PageCount);
  finally
    Lib.Free;
  end;
end;

fallback یک محافظ دارد که ارزش کپی کردن دارد. تلاش دوم در DecryptFile تنها زمانی اجرا می‌شود که فرم‌های آماده‌شده و خام متفاوت باشند و تلاش اول هیچ کد خطای سختی گزارش نکرده باشد. یک شکست ساختاری یعنی ورودی آسیب‌دیده است یا revision رمزنگاری‌ای نیست که فرض کرده‌اید، و تلاش دوباره یک فایل خراب با رمزعبور دیگر صرفاً یک parse کامل دیگر روی ورودی خصمانه می‌سوزاند؛ استدلال پشت آن رفلکس در یادداشت درباره parse ایمن PDFهای غیرقابل‌اعتماد بیان شده. توجه کنید که در سمت نوشتن هیچ fallbackای وجود ندارد، و آن نامتقارنی عمدی است. خواندن تاریخ را تحمل می‌کند، نوشتن نه: هر فایل AES-256 جدید بایت‌های مطابق مشخصات را می‌گیرد

کدام رمزعبورها کاملاً رد می‌شوند و خطای ۶۰۴ چیست؟

SASLprep می‌تواند یک رمزعبور را کاملاً رد کند، و وقتی این کار را می‌کند، رمزنگاری باید بلند شکست بخورد نه اینکه خاموش چیزی جایگزین کند. Encrypt و EncryptFile هر وقت Strength برابر ۳ یا ۴ باشد هم رمزعبور owner و هم user را آماده می‌کنند، در صورت رد شدن ۰ برمی‌گردانند، و LastErrorCode را روی PDFLIB_ERROR_PASSWORD_SASLPREP تنظیم می‌کنند، که ۶۰۴ است. دو خانواده ورودی آن را فعال می‌کنند. جداول خروجی ممنوع کاراکترهای کنترلی (C.2.1 و C.2.2)، code pointهای private-use (C.3)، non-characterها (C.4)، surrogateهای تنها (C.5)، U+FFFD (C.6)، کاراکترهای توصیف ایدئوگرافیک (C.7)، و بازه‌های کنترل نمایش و tagging (C.8 و C.9) را رد می‌کنند. جداگانه، قانون دوجهته بخش ۶ RFC 3454 هر رشته‌ای حاوی یک کاراکتر RandALCat از جدول D.1 را رد می‌کند مگر رشته هم با آن شروع و هم پایان یابد و هیچ حرف چپ‌به‌راستی اصلاً نداشته باشد

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // U+0007 is a C.2.1 control character, so preparation refuses the password
    if Lib.Encrypt('owner', 'bad' + WideChar($0007), 4,
         Lib.EncodePermissions(1, 0, 0, 0, 0, 0, 0, 1)) = 0 then
    begin
      if Lib.LastErrorCode = PDFLIB_ERROR_PASSWORD_SASLPREP then  // 604
        ShowMessage('The password contains characters that PDF encryption does not permit.');
    end;
  finally
    Lib.Free;
  end;
end;

آن قانون دوجهته همان چیزی است که میز پشتیبانی شما را غافلگیر خواهد کرد. یک رمزعبور عربی یا عبری که با یک رقم غربی پایان می‌یابد، یا یکی با یک حرف لاتین سرگردان در وسط، طبق مشخصات رد می‌شود حتی اگر در فیلد ورودی کاملاً معقول به‌نظر برسد. خطای ۶۰۴ را به‌عنوان پیامی درباره کاراکترهای رمزعبور نشان دهید، نه به‌عنوان یک شکست عمومی رمزنگاری، وگرنه کسی یک بعدازظهر را صرف جستجوی یک باگ در اشتقاق کلید شما می‌کند

محدودیت‌های صادقانه: NFKC، یک LCat تقریبی، و یک دام Delphi

دو بخش از پیاده‌سازی تقریب هستند، و هر دو شایسته بیان صریح‌اند نه دفن‌شدن. نرمال‌سازی NFKC توسط API ویندوز NormalizeString انجام می‌شود، که به‌صورت پویا از Normaliz.dll بارگذاری می‌شود. وقتی آن کتابخانه در دسترس نباشد، رشته نگاشت‌شده به‌صورت نرمال‌نشده استفاده می‌شود، که یعنی مراحل نگاشت و منع همچنان اجرا می‌شوند اما تای‌کردن سازگاری اجرا نمی‌شود. در عمل این DLL از Vista به بعد با هر نسخه ویندوز عرضه شده، بنابراین مسیر تنزل‌یافته یک دغدغه پیش از Vista و غیر ویندوز است نه یک دغدغه زنده، اما رمزعبوری که به تای‌کردن NFKC متکی باشد آنجا بایت‌های متفاوتی تولید می‌کند و آن یک واگرایی واقعی، هرچند دور، است. بررسی دوجهته تقریب دوم است: تشخیص کاراکترهای LCat از بازه‌های حرف رایج استفاده می‌کند به‌جای جدول کامل D.2 در RFC 3454، و جهت آن خطا همان چیزی است که آن را قابل‌قبول می‌کند. یک کاراکتر LCat جاافتاده فقط می‌تواند باعث شود قانون دوجهته جایی عبور کند که مشخصات آن را رد می‌کرد، هرگز برعکس، و هرگز به مراحل نگاشت یا نرمال‌سازی دست نمی‌زند، بنابراین توالی بایت آماده‌شده یک رمزعبور پذیرفته‌شده بدون تغییر است. ریسک باقی‌مانده بنابراین یک واگرایی خط‌مشی است نه یک واگرایی بایت: یک رمزعبور با اسکریپت غیرمعمول که یک پیاده‌سازی سخت‌گیرانه‌تر اصلاً از پذیرفتنش امتناع می‌کند. هر رمزعبوری که هر دو طرف می‌پذیرند یکسان هش می‌شود، که همان ویژگی‌ای است که تعامل‌پذیری واقعاً به آن وابسته است

در نهایت، یک دام syntax در Delphi که یک ساعت هزینه دارد اگر پیش از این با آن برخورد نکرده باشید. وقتی یک تابع یک نوع procedural برمی‌گرداند، انتساب آن بدون پرانتز آن را فراخوانی نمی‌کند. کامپایلر Proc := GetNormalizeProc; را به‌عنوان گرفتن آدرس خود GetNormalizeProc می‌خواند، سپس E2009 را با شکایت بی‌فایده اینکه calling conventionها متفاوتند گزارش می‌دهد، چون accessor از convention پیش‌فرض استفاده می‌کند درحالی‌که نوع API imported شده stdcall است. پرانتزهای خالی اجباری‌اند

type
  TNormalizeString = function(NormForm: Integer; SrcString: PWideChar; SrcLength: Integer;
    DstString: PWideChar; DstLength: Integer): Integer; stdcall;

function GetNormalizeProc: TNormalizeString;   // loads Normaliz.dll on first use
...
var
  Proc: TNormalizeString;
begin
  // Proc := GetNormalizeProc;   // E2009: reads as @GetNormalizeProc, conventions differ
  Proc := GetNormalizeProc();    // correct: calls the accessor and assigns its result
  if not Assigned(Proc) then
    Exit;                        // no NFKC available, mapped string is used as-is
end;

آماده‌سازی رمزعبور یکی از آن جزئیاتی است که هرگز در فهرست ویژگی‌ها ظاهر نمی‌شود و تعیین می‌کند آیا یک سند رمزنگاری‌شده تماس با مشتری در locale دیگری را از سر می‌گذراند یا نه. نقاط ورود Encrypt، EncryptFile، DecryptFile و SetPassword که اینجا شرح داده شد بخشی از losLab PDF Developer Library Pascal Edition برای Delphi و C++Builder هستند، که صفحه محصولش مرجع کامل رمزنگاری و جدول کامل کدهای خطا را حمل می‌کند