مقال تقني

بطء تحميل PDF الموقعة AES-256 في Delphi: إصلاحات HotPDF

يحمّل HotPDF ملفات PDF الموقعة المشفرة AES-256 داخل Delphi أسرع بكثير مما كان، لأن ثلاث كلف منفصلة أزيلت من مسار التحميل: تحليل رموز التدفق المصدر بلا تخزين مؤقت، وتشغيل ثانٍ لفحص كلمة المرور Algorithm 2.A من ISO 32000-2 أثناء فك التشفير، وحلقة AES-128-CBC داخلية بطيئة داخل تجزئة R6 المقساة. وعلى أهداف Delphi خفّض إصلاح R6 وحده تحميل مستند من نحو 66 ms إلى 16 ms

لم يظهر أي منها في ملفات الاختبار الصغيرة التي تستخدمها معظم المجموعات. عينة غير موقعة وغير مشفرة من كيلوبايت أو اثنين تفتح في أجزاء من مئة من الثانية أيّاً كان ما يفعله المحلل. وتبدأ المتاعب مع المستندات التي يرسلها العملاء فعلاً: عقد بتوقيع CMS مضمّن، نحو 35 KB، محمي بكلمة مرور مراجعة 6. تشغّل تلك التركيبة كل مسار كود مكلف دفعة واحدة، وكان لكل منها سببه الخاص في أن يكون أبطأ مما يلزم. تسلك هذه المقالة تلك المسارات بترتيب اكتشافها، مع أخطاء مفاتيح المخزن المؤقت التي برزت حين بدأت نتائج التحقق من التواقيع تُخزَّن

لماذا يحتاج ملف PDF موقع بحجم 34 KB إلى 170 ms للتحميل؟

احتاج ملف PDF موقع بحجم 34 KB إلى 170 ms للتحميل لأن المحلل كان يقرأ THandleStream مجردة مباشرة، وكان كل بايت رمز يتحول إلى استدعاء نظام ReadFile خاص به. مُرمِّز PDF يتقدم بايتاً بايتاً وهو يبحث عن الفواصل، فالتدفق المقبض بلا تخزين مؤقت يحول مسحاً رخيصاً في الذاكرة إلى آلاف الانتقالات إلى النواة

التفاصيل تفسر لماذا نجا الخطأ. كانت THotPDF.LoadFromFile تغلف مصدر ملفها في مخزن قراءة داخلي منذ زمن، وأصلح مجس AutoDecrypt سلفاً للسبب نفسه بعد أن أنفق 1.4 s في إعادة الاستعلام عن حجم ملف 56 MB. لكن المستندات الموقعة والمشفرة تسلك مساراً مختلفاً. ولكي تُضمن أن قراءات الكائنات الكسولة وفحص التواقيع ترى المراجعة غير القابلة للتغيير نفسها دوماً، حتى بعد أن يحرر المستدعي تدفقه الخاص، تأخذ THotPDF.LoadFromStream لقطة من المصدر وتحللها مرة ثانية. ذلك التحليل الثاني، مع تحميل إعادة الاستخدام المستخدم للمراجعات الموثقة، كان يقرأ مقبض اللقطة بلا مخزن أمامه. وكلاهما يجري الآن عبر قارئ المخزن الداخلي نفسه الذي يسلكه مسار الملف

أنبوب تحميل PDF موقع مشفر AES-256 في HotPDF داخل Delphi: تدفق لقطة، وتحليل بنيوي مخزن، ومسح مسبق Algorithm 2.A، وفك تشفير واحد يعيد استخدام المفتاح المستعاد، وتجزئة R6 مقساة تستدعي AES-128-CBC الأصلي، فتخفض التحميل من 66 ms إلى 16 ms
تتراكم الإصلاحات الثلاثة على مسار واحد: التخزين المؤقت يزيل آلاف استدعاءات النظام، ويُعاد استخدام المفتاح الممسوح مسبقاً تحت علم نطاق، وتصل الحلقة الداخلية لـ R6 إلى خلفية CBC الأصلية

ذلك المخزن داخلي، فلن تنشئه أنت أبداً. ما تتحكم فيه هو التدفق الذي تسلمه إلى LoadFromStream، ويقرأ HotPDF ذلك التدفق كما هو أثناء الممر البنيوي الأول. إن كانت البيانات في الذاكرة أصلاً فمرر TMemoryStream. وإن كانت على القرص فاستدعِ LoadFromFile، أو على Delphi 10.1 Berlin وما بعده أعطِ المحمّل TBufferedFileStream بدل TFileStream خام

uses
  System.SysUtils, System.Classes, HPDFDoc;

function LoadEncryptedFromBytes(const FileName: string;
  const Password: AnsiString): THotPDF;
var
  Mem: TMemoryStream;
begin
  Result := THotPDF.Create(nil);
  Mem := TMemoryStream.Create;
  try
    Mem.LoadFromFile(FileName);   // قراءة كتلية واحدة بدل قراءات لكل رمز
    Mem.Position := 0;
    // تعيد LoadFromStream عدد الصفحات، و0 عندما لم يُحمَّل المستند
    if Result.LoadFromStream(Mem, Password) < 1 then
      raise Exception.Create('Could not load ' + FileName);
  except
    Result.Free;
    Mem.Free;
    raise;
  end;
  Mem.Free;
end;

تحرير تدفق الذاكرة مباشرة بعد التحميل آمن لمدخلات موقعة ومشفرة، لأن المكون يحتفظ أصلاً بلقطته الخاصة من البايتات. احتفظ بالملف الأصلي أو المخزن على أي حال إن كنت ستتحقق من التواقيع لاحقاً، لأن التحقق يجزئ مقاطع /ByteRange الخام من الملف لا نموذج الكائنات المفكوك

قياس التحميل والتحقق كمرحلتين منفصلتين

ينبغي قياس زمن التحميل وزمن التحقق من التواقيع منفصلين، لأنهما يفشلان بطريقتين مختلفتين ويصلحان في موضعين مختلفين. تغطي مرحلة التحميل التحليل وفحص كلمة المرور وفك التشفير. وتعيد مرحلة التحقق تجزئة نطاقات البايتات الموقعة وتفحص توقيع CMS. وإن طويتها في رقم واحد اختبأ انحدار إحداها داخل الأخرى

يكفي في ذلك TStopwatch من RTL في System.Diagnostics. يحمّل الهيكل أدناه بالصيغة الزائدة بكلمة مرور من THotPDF.LoadFromFile ثم يتحقق من أول توقيع عبر الصيغة الزائدة TStream من VerifyLoadedSignature. الصيغة الزائدة بالتدفق هي الاختيار الصحيح لملف مشفر، لأن مسار تحميل كلمة المرور لا يحفظ اسم ملف مصدر لتصدّقه الصيغ بلا وسائط عند إعادة فتحه

uses
  System.SysUtils, System.Classes, System.Diagnostics,
  HPDFDoc, HPDFSignVerify;

procedure TimeSignedEncryptedLoad(const FileName: string;
  const Password: AnsiString);
var
  Pdf: THotPDF;
  Src: TFileStream;
  Info: THPDFSignatureInfo;
  Status: THPDFSignatureVerifyStatus;
  Watch: TStopwatch;
  Pages: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Watch := TStopwatch.StartNew;
    Pages := Pdf.LoadFromFile(FileName, Password);
    Writeln(Format('Load + decrypt: %d ms, %d pages, encrypted=%s',
      [Watch.ElapsedMilliseconds, Pages, BoolToStr(Pdf.IsEncrypted, True)]));
    if (Pages < 1) or (Pdf.GetLoadedSignatureFieldCount = 0) then
      Exit;

    Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
    try
      Watch := TStopwatch.StartNew;
      Status := Pdf.VerifyLoadedSignature(0, Src, Info);
      Writeln(Format('Verify signature 0: %d ms, status %d, signer %s',
        [Watch.ElapsedMilliseconds, Ord(Status), string(Info.SignerName)]));
    finally
      Src.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

شغّله عدة مرات وتجاهل التشغيل الأول. الاستدعاء الأول يدفع كلف إعداد تُدفع مرة واحدة، منها بناء جداول AES وإجراء اختبارها الذاتي، كما يُزيّف تخزين القرص المؤقت القراءة الأولى. ما تبحث عنه هو الرقم المستقر لملف عميل حقيقي، مقاساً مقابل نسخة غير موقعة وغير مشفرة من المستند نفسه

لماذا اشتققت كلمة مرور AES-256 مرتين؟

اشتققت HotPDF مفتاح ملف AES-256 مرتين لأن مرحلتين من التحميل نفسه كانتا تجريان فحص كلمة المرور الكامل كل واحدة بنفسها. قبل التحليل البنيوي يمسح RecoverReloadKeysFromStreamRanges البايتات الخام مسبقاً كي تفك حاويات تدفقات الكائنات المشفرة بينما تفك المرشحات ترميزها. ويجري ذلك المسح المسبق أصلاً فحوصات Algorithm 2.A من ISO 32000-2 §7.6.4.3.3، مصدقاً كلمة المرور مقابل ملحي المستخدم والمالك ومستعاداً مفتاح تشفير الملف. ثم تجاهلت DecryptLoadedDocument تلك النتيجة وأعادت الاشتقاق كله

في المراجعة 4 والأقدم كلفت النسخة المكررة ما يتجاوز حلقة MD5 قليلاً. وفي المراجعة 6 تستدعي Algorithm 2.A تجزئة Algorithm 2.B المقساة، فضاعفت النسخة المكررة أغلى جزء في فتح الملف. إعادة استخدام المفتاح المستعاد بسيطة. أما إعادة استخدامه بأمان فتلك هي الجزء الأصعب، لأن مفتاحاً مخزناً يعمّر أطول من فحص كلمة مروره يتحول إلى تجاوز مصادقة

يحد الإصلاح إعادة الاستخدام بعلم نطاق يكون صادقاً فقط بين المسح المسبق واستدعاء فك التشفير داخل الصيغ الزائدة بكلمة مرور من LoadFromFile و LoadFromStream، ويُمسح في كتلة finally. ولا يُرفع العلم إلا عندما يصدّق المسح المسبق كلمة المرور فعلاً. الاستدعاء المباشر لـ DecryptLoadedDocument بعد تحميل عادي يجري الاشتقاق الكامل دوماً، لأن تلك الكلمة لم تمسح مسبقاً قط. ومن دون ذلك القيد كان يمكن لمفتاح إعادة تحميل قديم من فتح ناجح سابق أن يفك المستند بصمت لمستدعٍ لاحق قدّم كلمة مرور خاطئة، وذلك خطأ أسوأ بكثير من تحميل بطيء

أين تنفق تجزئة R6 المقساة وقتها؟

تنفق تجزئة R6 المقساة كل وقتها تقريباً في تشفير AES-128-CBC. تكرر Algorithm 2.B من ISO 32000-2 §7.6.4.3.4 جولة 64 مرة على الأقل. تبني كل جولة مخزناً من 64 نسخة من كلمة المرور والتجزئة الحالية و، في جانب المالك، سلسلة /U، وتشفر ذلك المخزن بـ AES-128-CBC مستخدمة أول 16 بايت من التجزئة مفتاحاً والتالية 16 متجه تهيئة، وتنتقي SHA-256 أو SHA-384 أو SHA-512 للتجزئة التالية من أول 16 بايت من النص المشفر مقروءة عدداً بنمطية 3. التجزئات قصيرة. أما المخازن المشفرة فعدة كيلوبايتات في كل جولة

مخطط حلقة تجزئة Algorithm 2.B المقساة في R6 لـ HotPDF داخل Delphi: تبني كل جولة من 64 جولة على الأقل مخزناً من 64 نسخة من كلمة المرور والتجزئة وسلسلة /U، وتشفره بـ AES-128-CBC مستخدمة التجزئة مفتاحاً ومتجه تهيئة، وتنتقي SHA-256 أو SHA-384 أو SHA-512 من أول بايتات النص المشفر
كلفة R6 تعيش في تشفير المخزن بـ AES-128-CBC لا في البصمات، ولهذا خفّض روتين CBC أصلي داخل PDF20HashDanceR6 تحميل مستند كامل من نحو 66 ms إلى 16 ms

كان تنفيذ HotPDF لذلك الحلقة، أي PDF20HashDanceR6، يشفر المخزن كتلة كتلة عبر AES مرجعي موجّه بالبايت. يجرب الآن أولاً روتين CBC أصلياً بلا حشو، يصل على أهداف Delphi إلى خلفية AES المكتوبة بـ C المرتبطة. وبهذا التغيير خفض تحميل مستند كامل من نحو 66 ms إلى 16 ms. وإن تعذر الروتين الأصلي أو أعاد طولاً غير مطابق عاد الكود إلى حلقة الكتل التاريخية، ويمكن لتلك الحلقة بدورها استخدام كتلة AES-128 مدفوعة بالجداول تُولَّد جداولها T من صندوق S القائم. الشيفرة واحدة في كل الحالات، فالمفتاح المشتق مطابق بايتياً أيّاً كان المسار الذي حسبه

هذه المقالة عن السرعة. ضبط قاعدة إنهاء Algorithm 2.B صحيحاً مشكلة صواب منفصلة، والخطأ بفارق واحد هناك يرفض كلمات مرور صحيحة بعض الوقت فقط. أما جانب الإعداد من مفاضلة R5 مقابل R6 فمغطى في دليل إعداد تشفير PDF بـ AES-256 مع HotPDF

أربع طرق تخدعك بها كتلة AES سريعة

يمكن لكتلة AES مدفوعة بالجداول أن تنتج نصاً مشفراً خاطئاً وتمر باختبارها الذاتي مع ذلك. كل فخ من الفخاخ الأربعة أدناه برز أثناء بناء المسار السريع، واثنان منهما تجاوزا الاختبار الذاتي كما صُمم أول مرة

  • إعادة دخول الاختبار الذاتي. يرفع روتين الجاهزية علم تهيئته قبل إجراء الاختبار الذاتي، فالاستدعاء المعاد أثناء الاختبار يسلك المسار المرجعي. قارنت أول نسخة من الاختبار تنفيذ المرجع بنفسه إذن. الإصلاح كتلة سريعة خام بلا حراسة يستدعيها الاختبار الذاتي مباشرة، فيكون مسار الجداول هو الكود الذي يُختبر فعلاً
  • تخطيطات جداول مبدلة. تبديل ترتيب بايتات اثنين من جداول الجولة الأربعة (Te1 و Te3) ينتج نصاً مشفراً خاطئاً لم يكن للاختبار الذاتي المعاد أن ينتبه إليه. ما القطعه برنامج استقصاء مستقل طبع النص المشفر للملحق C.1 من FIPS-197 وهو 69c4e0d86a7b0430d8cdb78070b4c55a للمفتاح 000102…0f والنص الصريح 00112233…ff. يشترط الاختبار الذاتي الآن كلاً من ذلك المتجه والمساواة البايتية مع الكتلة المرجعية قبل أن يثق بالمسار السريع
  • فحوصات المدى والفيض في FPC. تحت Free Pascal مع -Cr و -Co ترفع التحويلات الصريحة بنمط ديلفي Byte() وحساب الإزاحة بعرض بايت استثناءات مدى أو فيض. تحسب مؤشرات الجداول ومضاعفة GF(2^8) الآن بعرض العدد الصحيح الأصلي وتقنَّع بـ and $FF
  • ترجمة خاطئة من المضمّن في FPC. مساعدات تحميل وتخزين صغيرة big-endian تأخذ معاملات PByte تُرجمت إلى انتهاكات وصول حين وُسمت inline تحت FPC. أزالة inline أصلحها، وكلفة الاستدعاء مهملة أمام جلب الجداول

التجربة نفسها خلف التوصيل النهائي. توجيه كل كتلة AES-128 في المكتبة عبر الجداول السريعة سبب انتهاك وصول حتمياً في FPC أثناء تحميل مرشح نشر واحد، فتراجع ذلك التغيير. التصميم المشحون محافظ: CBC الأصلي أولاً، وفرع جداول سريعة صريح داخل حلقة R6، والكتلة المرجعية في كل موضع آخر. والدرس العام أن كوداً تشفيرياً جديداً يجب أن يُبنى تحت عدة مصرفات يحتاج برنامج استقصاء مستقلاً بأجوبة معلومة يجري على كل مصرف هدف. فشل اختبار على مستوى DLL لا يخبرك أتملك جدولاً خاطئاً أم انهياراً. وللمزيد عن جانب Free Pascal انظر ملاحظات خلفيات Deflate و AES في HotPDF تحت Free Pascal

كيف تخزن التحقق من التواقيع دون تخزين جواب خاطئ؟

مخزن التحقق من التواقيع لا يكون آمناً إلا إذا غطى مفتاحه كل مدخل يستطيع تغيير الحكم، بما في ذلك مدخلات تعيش خارج نطاقات البايتات الموقعة. تخزن طبقة المستند اللا رأسي في HotPDF نتائج تحقق CMS بتجزئة بايتات CMS، وبايتات قاموس التوقيع في فجوة /Contents، ومحتويات النطاقات المغطاة، وبصمة سياسة، وميزانيات التحقق، وحجم المصدر الكلي، ودلو زمني من دقيقة واحدة. أُضيفت ثلاثة من تلك المدخلات بعد أن أظهر خطأ أنها غائبة فقط

مدخلات مفتاح مخزن التحقق من التواقيع في HotPDF لـ Delphi: تجزأ معاً بايتات CMS وقاموس التوقيع في فجوة /Contents ومحتويات النطاقات المغطاة وبصمة السياسة والميزانيات وحجم المصدر الكلي ودلو زمني من دقيقة قبل تخزين حكم CMS
لا يُعاد استخدام الحكم إلا عندما يكون كل مدخل يستطيع تغييره في المفتاح - فحجم المصدر مهم لأن مراجعة أساسية وتحديثها التدريجي قد يتشاركان نطاقات مغطاة مطابقة

تؤرشف ملفات الشهادات الموثوقة بالمحتوى لا بالمسار. تكتب طبقة العمليات مواد PEM نفسها في اسم ملف مؤقت جديد لكل أمر، فلم يطابق مفتاح قائم على المسار قط ولم يصب المخزن قط. ولا بد أن يكون حجم المصدر الكلي جزءاً من المفتاح لأن UnsignedTrailingBytes المبلَّغ عنه يعتمد عليه: قد تتشارك مراجعة أساسية وتحديث تدريجي لاحق المحتوى المغطى نفسه تماماً مع أطوال ذيلية مختلفة، ومخزن مفاتيحه النطاقات وحدها سيسلّم حكم المراجعة الأساسية للملف المحدث. وخلفية معنى تلك البايتات الذيلية في دليل HotPDF للتحقق من التواقيع الرقمية لملفات PDF داخل Delphi

كان الخطأ الثالث فخاً من لغة باسكال لا خطأ تصميم. روتينات التجزئة تأخذ مخزناً const بلا نوع، وتمرير متغير مصفوفة ديناميكية إلى معامل بلا نوع يمرر عنوان مرجع المصفوفة لا بياناتها:

function Checksum(const Data; Count: Integer): Cardinal;
var
  P: PByte;
  I: Integer;
begin
  Result := 0;
  P := @Data;
  for I := 0 to Count - 1 do
  begin
    Result := Result + P^;
    Inc(P);
  end;
end;

procedure DigestCMS(const CMS: TBytes);
begin
  // خطأ: يقرأ مرجع المصفوفة وما يليه على المكدس.
  // قراءة 16 KB من هناك تتجاوز صفحة المكدس وترفع انتهاك وصول،
  // ويفشل Delphi و FPC في نقاط مختلفة.
  Writeln(Checksum(CMS, Length(CMS)));

  // صحيح: مرر العنصر الأول، بشرط المصفوفة الفارغة
  if Length(CMS) > 0 then
    Writeln(Checksum(CMS[0], Length(CMS)));
end;

قفل المخزن هو SyncObjs.TCriticalSection لا الـ API ‏InitializeCriticalSection في Windows، لأن الوحدة نفسها تُبنى أيضاً تحت FPC على Linux حيث لا يوجد ذلك الـ API

ما يبقى بطيئاً عمداً

اشتقاق مفتاح R6 نفسه لا يُخزَّن قط عبر التحميلات، وذلك مقصود. البايتات الخارجية وكلمة المرور الموراة صراحة يسلكان مسار المصادقة الحقيقي دوماً، فيدفع كل تحميل بكلمة مرور لمستند R6 ثمن تجزئة Algorithm 2.B واحدة على الأقل، وتجزئتين من جانب المستخدم ومن جانب المالك عندما تُجرَّب كلمة مرور المالك. إعادة الاستخدام المحددة النطاق الموصوفة أعلاه تزيل النسخة المكررة داخل تحميل واحد ولا شيء بعدها

  • يخص رقم 66 ms إلى 16 ms أهداف Delphi بخلفية AES الأصلية مرتبطة. تأخذ بناءات FPC الكتلة المدفوعة بالجداول، وتوليد كود FPC على Windows جرى أبطأ من 2 إلى 4 مرات من Delphi تحت التثبيتات نفسها
  • يربط مخزن التحقق بدلو زمني من دقيقة، ففحوصات متكررة للبايتات نفسها في نافذة قصيرة رخيصة، بينما لا يعمّر حكم مخزن أبعد من الدقيقة التي حُسب فيها
  • نُفذ مخزن سلطات للمراجعات المدمجة ونُقح ثم أُزيل. لم يسد فجوة FPC وجعل استدعاءات أمان Delphi أبطأ بقياس، وهي تذكير جيد بالقياس قبل الإبقاء على مخزن
  • يحتاج التحقق من التواقيع بايتات الملف المشفر الأصلية، فأبقِ المصدر متاحاً. فك التشفير الأسرع لا يفعل شيئاً لإزالة ذلك الشرط

FAQ

هل تُضعف إعادة استخدام المفتاح الممسوح مسبقاً فحص كلمة المرور؟

لا. تعيد HotPDF استخدام مفتاح الملف المستعاد فقط داخل استدعاء الصيغة الزائدة بكلمة المرور نفسه الذي صدّق كلمة المرور بفحوصات Algorithm 2.A الكاملة، ويُمسح علم النطاق في كتلة finally. واستدعاء DecryptLoadedDocument مستقلاً يجري الاشتقاق التام دوماً، فلا يمكن استعارة مفتاح متروك من فتح سابق بكلمة مرور خاطئة

لماذا لا تُخزَّن نتيجة تجزئة R6 بين التحميلات؟

تخزين التجزئة بين التحميلات سيعني قبول كلمة مرور دون فحصها مقابل الملف الذي يُفتح. تعامل HotPDF كل مصدر بايتات خارجي وكل كلمة مرور صريحة بوصفها مدخلاً غير موثوق يجب أن يصادق بنفسه، فجاء التسريع القابل للقياس من جعل Algorithm 2.B أسرع لا من تجاوزها

هل ينبغي تمرير TFileStream إلى LoadFromStream؟

فَضِّل LoadFromFile للملفات على القرص، لأنه يخزن داخلياً أصلاً. وعندما تحتاج الصيغة الزائدة بالتدفق مرر TMemoryStream أو TBufferedFileStream، لأن HotPDF يقرأ التدفق الذي تقدمه مباشرة أثناء الممر البنيوي الأول والتدفق المقبض الخام يكلف استدعاء نظام لكل قراءة صغيرة

التحميل المحمي بكلمة مرور وتشفير AES-256 R6 والتحقق من تواقيع CMS وبقية API للمستند المحمّل تُشحن معاً في مكتبة VCL أصلية واحدة لـ Delphi و C++Builder. تفاصيل الميزات وإصدارات IDE المدعومة على صفحة منتج HotPDF Component