یک PDF رمزنگاریشده با AES-256 که رمزعبور غیر-ASCII دارد، در برنامهای که آن را نوشته باز میشود و هیچجای دیگر. علت تقریباً همیشه یک مرحله آمادهسازی گمشده است: ISO 32000-2 §7.6.4.3.3 لازم میداند رمزعبور با پروفایل SASLprep از stringprep پردازش شود پیش از آنکه UTF-8 کد شود و هش گردد. PDF Library for Delphi، کتابخانه 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 روی رشته نرمالشده اعمال میشود
PDF Library for Delphi کل پروفایل را در واحد PDFlibSASLprep پیادهسازی میکند، که یک نقطه ورود واحد ارائه میدهد. PLSASLprepPassword رمزعبور خام را میگیرد، فرم آمادهشده را در یک پارامتر var مینویسد، و وقتی رمزعبور باید رد شود False برمیگرداند. این تابع عمداً روی مسیر خوشحال کامل است: یک رمزعبور صرفاً-ASCII بایتبهبایت یکسان برمیگردد، بنابراین هیچچیز درباره استقرارهای موجود تغییر نمیکند
uses
PDFlibSASLprep;
var
Prepared: WideString;
begin
// RFC 4013: نگاشت، سپس NFKC، سپس خروجی ممنوع، سپس قانون دوجهته
PLSASLprepPassword('I' + WideChar($00AD) + 'X', Prepared); // -> 'IX' B.1 حذف میکند SOFT HYPHEN را
PLSASLprepPassword('a' + WideChar($00A0) + 'b', Prepared); // -> 'a b' C.1.2 NBSP را به U+0020 نگاشت میکند
PLSASLprepPassword(WideString(WideChar($00AA)), Prepared); // -> 'a' NFKC حرف ORDINAL INDICATOR را تا میکند
PLSASLprepPassword(WideString(WideChar($2168)), Prepared); // -> 'IX' NFKC عدد رومی نه (ROMAN NUMERAL NINE) را تا میکند
PLSASLprepPassword('user', Prepared); // -> 'user' ASCII هرگز دستکاری نمیشود
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 هر دو جدول را نام میبرد و نمیگوید کدام برنده است، پس این یک ابهام واقعی در مشخصات است نه یک خطای خواندن. PDF Library for Delphi ابتدا عضویت C.1.2 را آزمایش میکند و بنابراین U+200B را به یک فاصله نگاشت میکند، که رفتاری است که سایر پیادهسازیهای stringprep پرکاربرد روی آن مستقر شدهاند؛ تطبیق با آنها تنها چیزی است که اینجا اهمیت دارد، چون هدف توافق بایتی با هر readerی است که مشتری استفاده میکند
خواندن فایلهای قدیمی: ابتدا آمادهشده، دوم خام
راهحل مشکل سازگاری خودش را ایجاد میکند. هر فایل AES-256 که پیش از این تغییر نوشته شده رمزعبور UTF-8 خام را هش کرده، بنابراین ساختن reader بهصورت کاملاً مطابق مشتریها را از آرشیو خودشان بیرون میاندازد. PDF Library for Delphi این را در سمت خواندن با امتحان دو نامزد به ترتیب حل میکند. 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');
// قدرت 3 و 4 دو مقدار AES-256 هستند؛ هر دو پیش از هشکردن آماده میشوند
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' چیزی است که SASLprep تولید کرده و هر readerی مطابق مشخصات محاسبه میکند،
// پس فرم ساده ASCII فایلی را که با فرم soft-hyphen ساخته شده باز میکند
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 یک کاراکتر کنترلی C.2.1 است، پس آمادهسازی رمزعبور را رد میکند
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; // Normaliz.dll را در اولین استفاده بارگذاری میکند
...
var
Proc: TNormalizeString;
begin
// Proc := GetNormalizeProc; // E2009: بهصورت @GetNormalizeProc خوانده میشود، conventionها متفاوتند
Proc := GetNormalizeProc(); // درست: accessor را فراخوانی کرده و نتیجهاش را انتساب میدهد
if not Assigned(Proc) then
Exit; // NFKC در دسترس نیست، رشته نگاشتشده همانطور که هست استفاده میشود
end;
آمادهسازی رمزعبور یکی از آن جزئیاتی است که هرگز در فهرست ویژگیها ظاهر نمیشود و تعیین میکند آیا یک سند رمزنگاریشده تماس با مشتری در locale دیگری را از سر میگذراند یا نه. نقاط ورود Encrypt، EncryptFile، DecryptFile و SetPassword که اینجا شرح داده شد بخشی از losLab PDF Developer Library Pascal Edition برای Delphi و C++Builder هستند، که صفحه محصولش مرجع کامل رمزنگاری و جدول کامل کدهای خطا را حمل میکند