خطأ bcrypt «أطول من 72 بايت» ولماذا تفشل كلمات المرور القصيرة أيضًا
مشكلتان مختلفتان تُنتجان الرسالة نفسها، وواحدة منهما فقط لها علاقة بكلمة مرورك.
إن كانت كلمة مرورك تتجاوز فعلاً حد الـ 72 بايت في bcrypt، فإن bcrypt يقرأ أول 72 بايت ويُهمل الباقي. جزّأنا كلمتَي مرور طول كل منهما 82 بايت وتتطابقان في أول 72 بايت، تحت مِلح (salt) ثابت. أنتجتا معًا $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S، وأعادت bcrypt.compareSync(p2, hash(p1)) القيمة true. أي أن كلمة المرور الثانية تدخل إلى حساب صاحب الأولى.
وإن كانت كلمة مرورك قصيرة بوضوح ومع ذلك يظهر لك هذا:
password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
فالرسالة مخطئة في تشخيص السبب. فتحت passlib 1.7.4 مع bcrypt 5.0.0، تكفي كلمة مرور طولها 14 بايت لإطلاقها.
الجاني هنا مسبار اختبار ذاتي ثابت طوله 255 بايت داخل passlib. يعمل مرة واحدة عند تهيئة الواجهة الخلفية (backend)، قبل أن تصل كلمة مرورك إلى دالة التجزئة أصلاً. يرفض bcrypt 5.0.0 هذا المسبار، فيتسرّب الاستثناء إليك، فتقرأ شكوى عن كلمة مرور لم يكتبها أحد.
وترقيع monkey patch الخاص بـ __about__، وهو الذي يهيمن على نتائج البحث عن هذا الخطأ، لا يُصلح المشكلة. أعدنا تجربته في عملية نظيفة مع تطبيق الترقيع قبل import passlib، فعاد ValueError كما هو دون تغيير.
فرز في 30 ثانية: أيّ الحالتين أنت
| كلمة مرورك | متى يظهر الخطأ | السبب الجذري | اذهب إلى |
|---|---|---|---|
| أطول من 72 بايت | عند استدعاء دالة التجزئة | طويلة فعلاً. bcrypt 5.0 يرمي استثناءً، وbcrypt 4.x يقتطع بصمت | القسمان 2 و3 |
| أقصر من 72 بايت، وتستعمل passlib | عند أول استدعاء في العملية | مسبار الـ 255 بايت في passlib. لا علاقة له بكلمة مرورك | القسم 4 |
| تحتوي حروفًا صينية أو يابانية أو رموز emoji | تبدو قصيرة لكنها ليست كذلك | الأحرف ليست بايتات | القسم 3 |
| بدأت تفشل بعد ترقية اعتمادية | بعد النشر | تغيير كاسر في bcrypt 5.0 | القسمان 4 و5 |
إن كنت في الصف الثاني فتجاوز ما يليه. لن يفيدك شيء في القسمين التاليين، والحل مختلف تمامًا.
ماذا يفعل حدّ الـ 72 بايت في bcrypt بكلمة مرورك
لماذا يتوقّف bcrypt عند 72
بُني bcrypt على خوارزمية Blowfish، وهو يُمرّر كلمة مرورك إليها بوصفها المفتاح. تُوسّع Blowfish مفتاحها إلى مصفوفة P-array من 18 مفتاحًا فرعيًا، عرض كل منها 32 بت. هذا يساوي 18 × 4 = 72 بايت من مادة المفتاح، وحلقة التوسيع تلتفّ عائدة إلى بداية المفتاح بمجرد أن تملأ الخانات الثماني عشرة كلها.
فالسقف إذن بنيوي. ليس تكاسلاً في التنفيذ ولا مخزنًا مؤقتًا قابلاً للضبط نسي أحدهم توسيعه. كل تنفيذ متوافق لـ bcrypt على أي منصة يحمل الحد نفسه، ولهذا يظهر لك الرقم 72 في Python وNode وGo وJava وPHP على حد سواء.
كلمتا مرور مختلفتان، تجزئة واحدة
اقتطاع bcrypt لكلمة المرور خاصية أمنية، لا مجرد إزعاج متعلق بالطول.
باستخدام bcryptjs 3.0.3 مع المِلح الثابت $2a$10$abcdefghijklmnopqrstuv، جزّأنا كلمتَي مرور طول كل منهما 82 بايت:
| كلمة المرور | القيمة | البايتات |
|---|---|---|
| p1 | "A"×72 + "XXXXXXXXXX" | 82 |
| p2 | "A"×72 + "ZZZZZZZZZZ" | 82 |
وأنتجتا البصمة نفسها:
$2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S
كلمتا مرور مختلفتان، تجزئة واحدة: true. والنتيجة المترتبة على ذلك:
bcrypt.compareSync(p2, hash(p1)) // true
من يعرف أول 72 بايت من عبارة مرور طويلة يستطيع أن يُلحق بها أي شيء ويُصادَق عليه. كل بايت بعد الحدّ يُسهم بصفر تمامًا في قوة التجزئة المخزّنة، مهما اجتهد مستخدموك في اختياره. وإن أردت مطابقة تجزئة لديك مع كلمة مرور مرشّحة دون كتابة سكربت، يمكنك توليد تجزئات bcrypt والتحقق منها داخل المتصفح ومعاينة السلوك نفسه بنفسك.
أين يقع الحدّ بالضبط
ضيّقنا نقطة القطع بايتًا بايتًا، مع الإبقاء على بادئة مشتركة وتغيير بايت واحد بعدها بالضبط:
| البايتات المتطابقة في البادئة | الاختلاف عند البايت N+1 | التجزئة نفسها؟ |
|---|---|---|
| 70 | 71 | false |
| 71 | 72 | false |
| 72 | 73 | true |
| 73 | 74 | true |
البايت رقم 72 لا يزال محسوبًا. والبايت 73 هو أول بايت لا يُحسب. القطع حادّ عند هذه الحافة ولا يوجد خلط جزئي حولها، ويمكنك التحقق من ذلك على مكتبتك محليًا في دقائق.
الأحرف ليست بايتات
يَعُدّ bcrypt بايتات UTF-8، بينما يكتب مستخدموك أحرفًا. في نطاق ASCII يتصادف تطابق الرقمين، وهذا بالضبط ما يجعل المشكلة تعضّ الفرق لحظة إطلاقها خارج السوق الناطق بالإنجليزية.
| نوع الحرف | مثال | بايت لكل حرف | 72 بايت تساوي |
|---|---|---|---|
| حروف ASCII اللاتينية | A | 1 | 72 حرفًا |
| الحروف الصينية Han | 密 | 3 | 24 حرفًا |
| الكانا اليابانية | あ | 3 | 24 حرفًا |
| emoji | 🔒 | 4 | 18 حرفًا |
| السيريلية | я | 2 | 36 حرفًا |
| الحروف الألمانية ذات العلامات | ü | 2 | 36 حرفًا |
تحقّقنا من الطرفين معًا: مع كلمة مرور صينية تُهمَل الاختلافات بعد الحرف الرابع والعشرين (true)، ومع كلمة مرور من emoji تُهمَل الاختلافات بعد الثامن عشر (true).
عبارة مرور صينية من 25 حرفًا تبدو سخية في حقل كلمة المرور. وهي قد تجاوزت الخط أصلاً. ومن يختار 20 رمز emoji يكون قد تخطّى الحد بحرفين دون أن يُخبره أحد يومًا.
قياس الطول بالبايت في شيفرتك
فحوص الطول المكتوبة على أساس عدد الأحرف ستمرّ بينما القيمة الفعلية تجاوزت الحد أصلاً. قِس البايتات:
# Python
len(pw.encode("utf-8"))
// Node.js
Buffer.byteLength(pw, "utf8")
// Go
len([]byte(pw))
في المتصفحات التي لا يتوفر فيها Buffer، تعطيك new TextEncoder().encode(pw).length الرقم نفسه. ضع هذا الفحص أمام استدعاء التجزئة وأعِد رسالة تحقق حقيقية، بدلاً من ترك المكتبة تقرّر عنك في الثالثة فجرًا. وإن كنت تراجع سياسة الحد الأدنى للطول في الوقت نفسه، فمقال كيف تُقاس قوة كلمة المرور فعليًا يشرح ما تشتريه لك قاعدة الطول وما لا تشتريه.
لماذا تفشل كلمات المرور القصيرة أيضًا: مسبار الـ 255 بايت في passlib
هذه هي الحالة التي تدفع أغلب الناس إلى محركات البحث: كلمة مرورك من أربعة عشر حرفًا، والمكتبة تصرّ على أنها تتجاوز 72 بايت.
إعادة إنتاج المشكلة
ثلاثة أسطر، على Python 3.14.5 مع bcrypt 5.0.0 وpasslib 1.7.4:
from passlib.hash import bcrypt
bcrypt.hash("short-password") # 14 bytes
# ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
أربعة عشر بايتًا تدخل، وشكوى عن 72 بايت تخرج. خطأ passlib مع bcrypt حقيقي، لكن الرقم الوارد فيه يصف شيئًا آخر تمامًا.
مكدّس الاستدعاء كاملاً
تتبّعنا ما يجري فعليًا داخل passlib 1.7.4، خطوة بخطوة:
- الاستدعاء الأول يُطلق تهيئة الواجهة الخلفية:
_calc_checksum←_stub_requires_backend()←set_backend(). - تقرأ
_load_backend_mixinالقيمةbcrypt.__about__.__version__. السمة غير موجودة، فيُرفعAttributeError. تبتلعه passlib وتطبع(trapped) error reading bcrypt version. - تستمر التهيئة إلى
_finalize_backend_mixin(passlib/handlers/bcrypt.py:421)، التي تستدعيdetect_wrap_bug(IDENT_2A). - تتحقّق
detect_wrap_bug(في الملف نفسه،:378) من مسبار ثابت طوله 255 بايت. - يرمي bcrypt 5.0.0 استثناء
ValueErrorلأي شيء يتجاوز 72 بايت، فينفجر المسبار في وجه نفسه. - يصعد الاستثناء إلى موضع استدعائك. فترى رسالة عن 72 بايت لم تكن يومًا عن مُدخلك.
يحدث التسلسل كله مرة واحدة لكل عملية، عند أول تجزئة أو أول تحقّق. ولهذا يتكرّر الفشل بهذه الموثوقية ولا يتأثر إطلاقًا بما تُمرّره.
كيف يبدو المسبار
secret = (b"0123456789" * 26)[:255]
هذا الثابت مصدره خلل الالتفاف في bcrypt الخاص بـ BSD، والذي أعلنته Openwall عام 2012، حيث كانت المفاتيح الطويلة تلتفّ وتنهار إلى تجزئات أضعف. تتحقّق passlib عند الإقلاع مما إذا كانت الواجهة الخلفية التي حمّلتها للتوّ تحمل هذا العيب، وترفض الوثوق بواجهة تحمله.
وdetect_wrap_bug ليست خللاً في passlib. إنها شيفرة دفاعية تؤدي تمامًا ما كُتبت لأجله، بمتجه اختبار ظلّ صالحًا أكثر من عقد. ما تغيّر هو أن bcrypt 5.0.0 صار يعامل مُدخلاً بطول 255 بايت بوصفه خطأ لا بوصفه شيئًا يُجزَّأ، وهو ما يحوّل اختبارًا ذاتيًا ناجحًا إلى اختبار لا يمكن التقاطه. والنقاش في المسألة رقم #1082 لدى pyca/bcrypt يغطي الاصطدام بين المكتبتين.
لماذا لا يُصلح ترقيع __about__ المشكلة
ابحث عن هذا الخطأ وسيُقال لك مرارًا وتكرارًا إن bcrypt أزال __about__ وإن إعادته تُصلح passlib. شطرا العبارة خطأ، والقياس التالي يُظهر ذلك:
| الإصدار | hasattr(bcrypt, "__about__") | يطبع تحذير trapped | هل تعمل passlib |
|---|---|---|---|
| bcrypt 5.0.0 | False | نعم | لا (ValueError) |
| bcrypt 4.3.0 | False | نعم | نعم |
الإصدار 4.3.0 لا يملك __about__ هو الآخر. وهو يطبع سطر (trapped) error reading bcrypt version نفسه. ومع ذلك تعمل passlib عليه دون شكوى. فالسمة الغائبة ليست إذن الخط الفاصل بين «يعمل» و«معطّل». الخط الفاصل هو تغيّر سلوك ValueError في 5.0.0.
ومعنى ذلك أن الترقيع الشائع لا يمكن أن ينجح، وهو فعلاً لا ينجح:
import bcrypt, types
bcrypt.__about__ = types.SimpleNamespace(__version__=bcrypt.__version__) # before importing passlib
from passlib.hash import bcrypt as pl
pl.hash("short-password")
# still ValueError: password cannot be longer than 72 bytes, ...
شغّلنا هذا في عملية نظيفة، مع تطبيق الترقيع قبل import passlib، تحديدًا كي لا يعزو أحد الفشل إلى ترتيب الاستيراد. ومع ذلك يفشل. كل ما يحقّقه الترقيع هو إسكات تحذير غير ضار. فمسبار الـ 255 بايت في الخطوة 4 مرحلة منفصلة لم تستشر __about__ من الأساس، وهو ينفجر في الحالتين.
ما الذي غيّره bcrypt 5.0 فعليًا
التغيير الكاسر في bcrypt 5.0 سطر سلوكي واحد، لكن أثره يمتدّ بعيدًا:
| المُدخل | bcrypt 4.3.0 | bcrypt 5.0.0 |
|---|---|---|
| 72 بايت | يعمل | يعمل |
| 73 بايت | يعمل (باقتطاع صامت) | ValueError |
| 100 بايت | يعمل (باقتطاع صامت) | ValueError |
| 255 بايت | يعمل (باقتطاع صامت) | ValueError |
والاقتطاع في عمود الإصدار 4.x ليس مجازًا. فتحت 4.3.0، تخرج hash(73 bytes) وhash(100 bytes) المبنيتان من البادئة نفسها متساويتين: true.
فـ bcrypt 5.0 هو المكتبة الأصحّ هنا. التخلّص الصامت من مادة المفتاح نتيجة أسوأ من رفض المتابعة، وهذا الرفض هو ما يجب أن تفعله مكتبة تجزئة حين تعجز عن الوفاء بالمُدخل الذي أُعطي لها. لكن الترقية تظلّ مؤلمة رغم ذلك. فشيفرة كانت تفقد بايتات بصمت لسنوات صارت ترمي استثناءات، وإن كان ذلك المسار يمرّ عبر passlib، فهو يرمي قبل أن يدخل مُدخلك في الحسبان أصلاً.
من ذلك الجدول ينتج أمران، وهما يقعان على فريقين مختلفين. إن كنت تستدعي bcrypt مباشرة فالترقية مرئية: يظهر لك استثناء عند التسجيل أو تسجيل الدخول، في موضع شيفرة تملكه، مع أثر مكدّس يشير إلى استدعاء التجزئة الخاص بك. أضِف فحصًا لطول البايتات أمامه وتنتهي المسألة في بعد ظهيرة واحدة.
أما إن كنت تمرّ عبر passlib فالترقية غير مرئية إلى أن تصير شاملة. الفشل ليس متناسبًا مع عدد مستخدميك ذوي كلمات المرور الطويلة، لأنه لا يعتمد على مُدخل المستخدم إطلاقًا. كل عملية تجزئة وكل تحقّق داخل العملية يفشلان، من الاستدعاء الأول فصاعدًا، في قاعدة شيفرة لم يتغيّر فيها شيء يخصّ معالجة كلمات المرور. لهذا يظهر الأمر بوصفه حادثة نشر لا تقرير خلل، ولهذا يرسل نصّ الخطأ الناس للبحث في المكان الخطأ بالضبط.
كيف تُصلحه
إن كان بوسعك تعديل الشيفرة
استغنِ عن passlib واستدعِ bcrypt مباشرة. آخر إصدار لـ passlib كان 1.7.4 والمشروع هادئ منذ زمن طويل، فالطبقة الوسيطة لا تشتري لك شيئًا يُذكر في مشروع لا يحتاج سوى bcrypt:
import bcrypt
password = "correct horse battery staple".encode("utf-8")
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
bcrypt.checkpw(password, hashed) # True
تأخذ hashpw وcheckpw كلتاهما بايتات، فرمِّز عند الحدّ وأبقِ بقية شيفرتك تعمل بنوع str. لا يجري هنا أي كشف عن واجهة خلفية ولا اختبار ذاتي عند التهيئة، فلن تصادف فشلاً يشتكي من كلمة مرور لم تُمرّرها أصلاً. وإن أردت معاينة التجزئة الناتجة أو التحقق من واحدة أنتجها تطبيقك، فإن مولّد bcrypt يعمل بالكامل داخل متصفحك. والخوادم التي تستخدم bcrypt في مصادقة HTTP Basic Auth تخضع للقيد البنيوي نفسه في صيغة ملف مختلفة، وهو ما يشرحه دليل htpasswd خطوة بخطوة.
إن تعذّر عليك تعديل الشيفرة اليوم
ثبّت الإصدار دون 5:
bcrypt<5
تحقّقنا من bcrypt 4.3.0 مع passlib 1.7.4 فوجدناه يعمل. لكن كن واضحًا بشأن ما اشتريته للتوّ. هذا ضماد مؤقّت لا علاج. أنت تبقى على إصدار سلوكه مع كلمة مرور طويلة في bcrypt هو التخلّص من البايتات بصمت، وهي المشكلة نفسها التي صدر 5.0 لإيقافها. ضَع تاريخًا لانتهاء هذا التثبيت وخطّط للانتقال.
إن كان مستخدموك يكتبون عبارات مرور طويلة فعلاً
جزّئ كلمة المرور أولاً بـ SHA-256، ثم رمِّز البصمة بـ base64، ثم مرّر الناتج إلى bcrypt:
import base64, hashlib, bcrypt
def prehash(password: str) -> bytes:
return base64.b64encode(hashlib.sha256(password.encode("utf-8")).digest())
hashed = bcrypt.hashpw(prehash(pw), bcrypt.gensalt(rounds=12))
bcrypt.checkpw(prehash(pw), hashed)
الناتج 44 بايتًا دائمًا، أي أقل من 72 بفارق مريح، مهما كان طول المُدخل. وهو يُعيد الخاصية التي دمّرها الاقتطاع: كلمتا المرور اللتان طول كل منهما 82 بايت من مطلع المقال تعطيان، بعد تمريرهما عبر هذا، checkpw(prehash(p2), hash(prehash(p1))) = False. اختفى التصادم.
خطوة base64 تؤدي عملاً حقيقيًا، فلا تُسقِطها. بصمة SHA-256 الخام بيانات ثنائية اعتباطية وقد تحتوي بايتات NUL، وتنفيذات bcrypt تتعامل معها بطرق غير متسقة. أما base64 فيمنحك سلسلة ASCII خالية من NUL وبطول ثابت. طبّق الدالة نفسها عند التسجيل وعند تسجيل الدخول، وإلا توقّفت كل تجزئة قائمة عن التحقق.
ما لا ينبغي فعله
ترقيع monkey patch لـ __about__ لا يعمل. القسم 4 يحمل القياس. وإن كان أحد أفراد فريقك على وشك لصقه، فالأسطر الأربعة أعلاه ستوفّر عليه بعد ظهيرة كاملة.
أما الاقتطاع بنفسك عبر pw[:72] فهو أسوأ من عدم فعل شيء. إنه يُحوّل الفشل الصاخب إلى فشل صامت، ويُعيد إنتاج التصادم من القسم 2 داخل شيفرتك أنت. ستكون تصنع يدويًا السلوك نفسه الذي صدر bcrypt 5.0 للقضاء عليه، وعلى خلاف إصدار المكتبة، نسختك لن تُحذّر أحدًا أبدًا. إن كنت تحتاج إلى دعم كلمات المرور الطويلة فجزّئها مسبقًا. وإن لم تكن تحتاجه، فتحقّق من طول البايتات وارفض برسالة واضحة.
ماذا عن التجزئات الموجودة في قاعدة بياناتك
أي السجلات متأثرة
الحسابات التي سجّل أصحابها بكلمة مرور تتجاوز 72 بايت فقط. في أغلب المنتجات الاستهلاكية هذه مجموعة صغيرة، وفي أي منتج بالإنجليزية فقط تعني عادةً هواة عبارات المرور. أما في المنتجات التي يكتب فيها المستخدمون بالصينية أو اليابانية أو emoji، فالقسم 3 ينطبق وقد تكون المجموعة المتأثرة أكبر بكثير مما يوحي به تدقيق أعمى عن البايتات.
ولا يمكنك تمييز هذه السجلات من التجزئات نفسها. فبصمة bcrypt ثابتة العرض ولا تحمل أي أثر لطول مُدخلها. إن كنت تسجّل طول كلمة المرور عند التسجيل فذلك السجل هو جردك الوحيد. أغلب الفرق لم تفعل، وإعادة بناء ذلك لاحقًا غير ممكنة، فخطّط على أساس أنك لا تعرف، لا على أساس قائمة.
لا يمكنك إعادة الحساب دفعة واحدة
لا يوجد نصّ صريح لإعادة تجزئته، وهذا هو جوهر تخزين التجزئات أصلاً. فالترحيل لا بد أن يكون كسولاً: رقِّ كل حساب في المرة التالية التي يُصادَق فيها صاحبه بنجاح، بينما تحتفظ بالنصّ الصريح في الذاكرة للحظة.
def login(user, password: str) -> bool:
if not verify_legacy(password, user.password_hash):
return False
if needs_rehash(user.password_hash):
user.password_hash = hash_new_scheme(password)
save(user)
return True
تحقّق بالمخطّط القديم أولاً، وعندها فقط أعِد التجزئة. عكس هاتين الخطوتين يكتب فوق التجزئة المخزّنة قبل أن تتأكد من صحة كلمة المرور. خزّن معرّف المخطّط بجوار كل تجزئة كي تكون needs_rehash مقارنة حقل لا تخمينًا، وتوقّع ذيلاً طويلاً من الحسابات الخاملة التي لن تسجّل دخولاً أبدًا. هذه تعالجها عند إعادة تعيين كلمة المرور، لا بالإجبار.
متى يستحق الترحيل الكامل
إن كنت تكتب مسار إعادة التجزئة الكسولة أصلاً، فهذه أرخص لحظة ستحصل عليها لتغيير الخوارزمية تحته. سقف الـ 72 بايت غير موجود في Argon2id، والمقارنة المعمّقة بين Argon2id وbcrypt تغطي متى يستحق التبديل ثمنه ومتى يكون البقاء على bcrypt هو القرار الصائب. وورقة OWASP المرجعية لتخزين كلمات المرور هي المرجع الذي تراجع عليه معاملاتك.
لا تبدأ ترحيلاً لمجرد هذا الخطأ. إن كانت كلمات مرورك أقل من 72 بايت بفارق مريح، فـ bcrypt يبقى اختيارًا سليمًا والقسم 6 حلّ مشكلتك أصلاً.
الأسئلة الشائعة
لماذا يقول bcrypt إن كلمة مروري أطول من 72 بايت وهي قصيرة؟
لأن الرسالة تتحدث عن المسبار الداخلي في passlib، لا عن كلمة مرورك. عند الاستدعاء الأول تُشغّل passlib الدالة detect_wrap_bug بسلسلة اختبار ثابتة طولها 255 بايت. يرمي bcrypt 5.0.0 استثناء ValueError لأي شيء يتجاوز 72 بايت، فيفشل المسبار ويطفو الخطأ إلى موضع استدعائك. وكلمة مرور من 14 بايت كافية لإطلاقه.
هل يتجاهل bcrypt فعلاً كل ما بعد 72 بايت؟
نعم، يتجاهل bcrypt كل بايت بعد 72 تمامًا. كلمتا مرور طول كل منهما 82 بايت وتتطابقان في أول 72 بايت تُنتجان التجزئة نفسها $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S، وكل منهما تتحقّق مقابل تجزئة الأخرى. والحدّ دقيق: اختلاف عند البايت 72 يغيّر التجزئة، واختلاف عند البايت 73 لا يغيّرها.
هل حدّ الـ 72 بايت مشكلة أمنية؟
نعم، حدّ الـ 72 بايت في bcrypt مشكلة أمنية بالنسبة إلى عبارات المرور الطويلة. من يعرف أول 72 بايت يستطيع إلحاق بايتات اعتباطية والمصادقة، فكل بايت بعد الحدّ لا يضيف شيئًا. أما لكلمات المرور دون 72 بايت فلا يغيّر شيئًا إطلاقًا. والتجزئة المسبقة بـ SHA-256 تُزيل هذا التعرّض إن وجب أن تُحسب المُدخلات الطويلة كاملة.
كم حرفًا تساوي 72 بايت؟
عدد الأحرف التي تسعها 72 بايت يعتمد على الترميز. 72 حرفًا لاتينيًا من ASCII، أو 36 حرفًا سيريليًا أو ألمانيًا ذا علامات، أو 24 حرفًا صينيًا من Han، أو 24 حرفًا من الكانا اليابانية، أو 18 رمز emoji. يَعُدّ bcrypt بايتات UTF-8 لا الأحرف، فقِس بـ len(pw.encode("utf-8")) في Python أو Buffer.byteLength(pw, "utf8") في Node.
هل يُصلح ترقيع __about__ خطأ passlib؟
لا، الترقيع لا يُصلح خطأ passlib مع bcrypt. طبّقنا الترقيع قبل import passlib في عملية نظيفة ومع ذلك انطلق ValueError. كما أن bcrypt 4.3.0 يفتقر إلى __about__ أيضًا ويعمل مع passlib دون مشكلة، وهو ما يثبت أن السمة الغائبة ليست السبب. الترقيع يُسكت فقط تحذير (trapped) error reading bcrypt version.
هل أُخفّض bcrypt إلى ما دون 5.0؟
كإجراء مؤقت، نعم، خفض bcrypt إلى ما دون 5.0 مقبول. bcrypt 4.3.0 مع passlib 1.7.4 يعمل. لكن الإصدار 4.x يقتطع بصمت كل ما يتجاوز 72 بايت، وهو السلوك الذي صدر 5.0 لإيقافه، فتعامل مع التثبيت بوصفه مؤقتًا وانتقل إلى استدعاء bcrypt مباشرة.
هل أقتطع كلمة المرور إلى 72 بايت بنفسي؟
لا تقتطع كلمة المرور إلى 72 بايت بنفسك. pw[:72] تُعيد إنتاج التصادم الموصوف أعلاه داخل شيفرتك، بصمت، ودون تحذير من المكتبة يلتقطه أحد. إما أن تُجزّئ مسبقًا بـ SHA-256 وbase64 كي تبقى المُدخلات الطويلة متمايزة، وإما أن تتحقّق من طول البايتات مسبقًا وترفض برسالة خطأ واضحة.
ماذا يحدث لكلمات المرور التي جُزّئت قبل إصلاح هذا؟
تبقى تجزئات bcrypt القائمة تتحقّق، لأن مسار التحقق لديك يقتطع بالطريقة نفسها التي اقتطع بها مسار التجزئة. الحسابات المسجّلة بكلمات مرور تتجاوز 72 بايت وحدها هي المُضعَفة، ولا يمكنك إعادة حسابها دون النصّ الصريح. أعِد التجزئة كسولاً عند أول تسجيل دخول ناجح، وعالج الحسابات الخاملة عند إعادة تعيين كلمة المرور.