Bcrypt

ولّد بصمة ‎bcrypt أو تحقّق من كلمة مرور مقابل بصمة قائمة دون مغادرة متصفحك: اختر الكلفة والنسخة ‎$2a$‎ أو ‎$2b$‎ أو ‎$2y$‎، واقرأ أي بصمة إلى أجزائها.

لا شيء تكتبه هنا يغادر هذا المتصفح. العمل يجري داخل ‎Web Worker في هذه اللسان، فافتح لوحة الشبكة عندك وراقب: لا يخرج أي طلب أثناء الحساب.

توليد بصمة

كلمة مرور وكلفة ونسخة. يُسحب الملح جديدًا من هذا المتصفح في كل تشغيل، فالكلمة نفسها تعطي بصمة مختلفة كل مرة — وهذا ‎bcrypt يعمل كما ينبغي لا خلل فيه.

التحقّق من كلمة مرور مقابل بصمة

البصمة الملصقة تُقرأ إلى أجزائها فورًا، بلا كلمة مرور وبلا انتظار. أضف كلمة المرور لتحصل على المطابقة. البصمة لا يمكن عكسها، لا هنا ولا في أي مكان.

ما الذي بداخل بصمة bcrypt فعلًا

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

  • النسخة، وهي الحقل الذي تفتتح به السلسلة: ‎$2b$‎ أو ‎$2a$‎ أو ‎$2y$‎. وهي تسمّي القواعد التي أنتجت البصمة.
  • الكلفة، مكتوبة برقمين اثنين بالضبط: من 04 إلى 31. وهي أسّ، فالكلفة 12 تعني ‎2^12 — 4,096 — جولة من مطّ المفتاح، والكلفة 13 ضعف عمل الكلفة 12 لا أكثر منها بجزء يسير.
  • الملح، اثنان وعشرون حرفًا: ستة عشر بايتًا عشوائيًّا، مخزّنة مكشوفة إلى جانب الجواب الذي ملّحته.
  • الملخّص، آخر واحد وثلاثين حرفًا: ثلاثة وعشرون بايتًا من المخرجات. وهو الجزء الوحيد الذي يعتمد على كلمة المرور، وهو ليس البصمة — البصمة هي السلسلة كلها.

والأبجدية التي تُؤخذ منها هذه الحروف أبجدية bcrypt نفسه، وليست Base64 القياسية: ترتيبها ‎./A-Za-z0-9، فالنقطة والشرطة المائلة تفتتحانها حيث تختم علامةُ الجمع والشرطةُ المائلة أبجديةَ Base64. ومفكّك Base64 عادي يعيد منها بايتات خاطئة بدل أن يعطي خطأ، وهذه طريقة معروفة لإضاعة بعد ظهر كامل.

الحدّ اثنان وسبعون بايتًا، وبالعربية يصل عند ستة وثلاثين حرفًا

يقرأ bcrypt اثنين وسبعين بايتًا من كلمة المرور على الأكثر ويتجاهل كل ما بعدها. لا اثنين وسبعين حرفًا — اثنين وسبعين بايتًا من UTF-8، فكم حرفًا يكون ذلك يتوقف على ما تكلّفه حروف اللغة: الحروف اللاتينية الستة والعشرون المجرّدة من العلامات تكلّف بايتًا واحدًا للحرف، وكل حرف سواها يكلّف أكثر. والحرف العربي يكلّف بايتين، فكلمة المرور بالعربية تُقطع عند ستة وثلاثين حرفًا: نصف العدد الذي يلقاه قارئ الإنجليزية، وعند طول يكتبه الناس فعلًا. وبالنسبة إلى هذا القارئ ليس هذا حدًّا نظريًّا بل حدًّا يُلاقى.

  • ASCII يكلّف بايتًا واحدًا للحرف، فيلقى المتحدث بالإنجليزية الحدّ عند اثنين وسبعين حرفًا ولا يكاد يبلغه أبدًا.
  • العبرية والعربية والروسية واليونانية تكلّف بايتين للحرف، فيصل الحدّ عند ستة وثلاثين حرفًا.
  • واليابانية والكورية والصينية تكلّف ثلاثة، فيصل عند أربعة وعشرين.
  • ومعظم الإيموجي يكلّف أربعة، وكثير مما يكتبه الناس منها مبنيّ من عدة نقاط ترميز موصولة بعضها ببعض، فحفنة منها هي الميزانية كلها.

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

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

النسخة تغيّر البادئة لا الملخّص

تبدو النسخة كرقم إصدار وليست كذلك. والثلاث التي تكتبها هذه الأداة غير مرتّبة، ولا واحدة منها تحلّ محلّ أخرى، والاختيار بينها قرار توافق لا قرار أمان.

  • ‎$2b$‎ هي ما استقرّ عليه OpenBSD وما تُخرجه اليوم Python وNode وGo. وهي الافتراضية هنا والجواب الصحيح حين لا يفرض شيء غيرها.
  • ‎$2a$‎ هي الأقدم، وما زالت عمليات نشر معمّرة من Java وSpring Security تتوقّع رؤيتها.
  • ‎$2y$‎ هي ما تكتبه password_hash في PHP ومن ثمّ Laravel، فالبصمة التي تُلصق في ذلك العالم تريدها عادةً.

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

وهناك اثنتان أخريان تسمّيهما هذه الأداة بدل أن تحسبهما. ‎$2x$‎ ليست إصلاحًا: سكّها Openwall عمدًا لإعادة إنتاج خلل في تمديد الإشارة، حتى تظلّ البصمات التي أنتجها الكود المعطوب قابلة للتحقّق، وتطبيقها هنا يعني وضع عيب معروف داخل الأداة نفسها. و‎$2$‎ هي الأصل، من قبل أن يُضاف بايت الصفر الأخير إلى المفتاح. ولا تكتب واحدةً منهما اليوم أداة عاملة؛ فإن كانت بيدك واحدة فقد خرجت من منظومة قديمة إلى حدّ أنّ النسخة أهون ما وجدت.

الكلفة، وكم تكلّف على جهازك أنت

الكلفة هي معامل العمل، من 4 إلى 31، وهي أسّ: كل خطوة تضاعف الزمن. وهذه غاية bcrypt كلها. فبصمة كلمة المرور يُراد لها أن تكون بطيئة، لأن مهاجمًا يحمل جدولًا مسروقًا يدفع الثمن نفسه على كل تخمينة، ومضاعفة الكلفة تنصّف عدد التخمينات في الثانية التي يحصل عليها بماله.

وما تستغرقه كلفة بعينها حقيقةٌ عن الآلة التي تشغّلها لا عددٌ يمكن كتابته سلفًا — فالهاتف والخادوم بينهما مرتبتان من حيث المقدار. ولذلك تقيس هذه الصفحة بدل أن تنقل: تشغّل بصمة قصيرة واحدة على جهازك أنت عند تحميل الصفحة وتستقرئ منها، ولهذا يبقى التقدير الذي بجانب عنصر التحكّم بالكلفة فارغًا لحظةً ثم يظهر. وفوق ثوانٍ معدودة تسأل قبل أن تبدأ، حتى لا يُقرأ رقم كُتب خطأً كصفحة متجمّدة.

ولاختيار عدد: 12 هي ما تُخرجه Laravel وحزمة bcrypt في Python افتراضيًّا، وهي أرضية إنتاج معقولة اليوم. والكلف الأدنى منها لتجهيزات الاختبار، حيث لا ينبغي لحزمة اختبارات تزرع خمسين مستخدمًا أن تنفق ثانية لكل مستخدم. والكلف فوق 15 تقريبًا تستحقّ القياس مقابل حركة تسجيل الدخول الحقيقية عندك قبل إطلاقها، لأن كل دخول يدفع الثمن أيضًا.

# Apache: كتابة سطر htpasswd بالكلفة 12
htpasswd -nbBC 12 alice "correct horse battery staple"

# PHP وLaravel: password_hash تُخرج $2y$
php -r 'echo password_hash("correct horse battery staple", PASSWORD_BCRYPT);'

# Python: حزمة bcrypt تُخرج $2b$، وgensalt تأخذ الكلفة
python -c "import bcrypt; print(bcrypt.hashpw(b'correct horse battery staple', bcrypt.gensalt(12)))"

الملح، ومتى تكون بصمة صُنعت هنا غير صالحة للتخزين

الملح ستة عشر بايتًا عشوائيًّا تُسحب جديدة لكل بصمة، وهو مخزّن مكشوفًا داخل السلسلة. وليس سرًّا ولم يُقصد به قط أن يكون سرًّا — وظيفته أن تنتج كلمتا مرور متطابقتان بصمتين لا صلة بينهما، حتى لا يسقط جدول مسروق أمام قاموس واحد محسوب سلفًا.

وهذا أيضًا سبب أن كلمة المرور نفسها تعطيك بصمة مختلفة مع كل ضغطة على الزرّ هنا. ليس هذا خللًا وليس الجواب السابق قد بطل: الملح الجديد هو bcrypt يعمل كما ينبغي. وكل واحدة من تلك البصمات تطابق كلمة المرور، لأن الملح يسافر داخل السلسلة التي يجري التحقّق منها.

وحقل الملح المتقدّم موجود لمهمة واحدة: إعادة إنتاج بصمة شخص آخر بالضبط، حتى يمكن وضع تطبيقين وجهًا لوجه. الصق فيه بصمة كاملة، فتتبعها الكلفة والنسخة تلقائيًّا، وتعيد كلمةُ المرور الصحيحة إنتاجَ المُدخل حرفًا حرفًا. وكل ما يُنتَج بهذه الطريقة فهو للمقارنة لا للتخزين — فملح يُستعمل مرتين ملحٌ كفّ عن أداء عمله.

بصمة bcrypt لا يمكن عكسها

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

وما يستطيعه موقع — وما تفعله المواقع التي تعلن فكّ التشفير — هو التخمين. تُؤخذ قائمة من كلمات المرور الشائعة، وتُبصم كل واحدة منها مقابل ملحك وعند كلفتك، ويُنظر هل تطابق واحدة منها. وقد صُمّم bcrypt ليجعل هذا بالذات مكلفًا، وهذا ما وُجدت الكلفة من أجله، وهذه الأداة لا تعرض ذلك بأي صورة.

والصيغة الصادقة لذلك السؤال سؤال آخر عادةً، وله جواب: ما الذي في هذه السلسلة، وهل تنتجها كلمة المرور هذه؟ وكلاهما على هذه الصفحة. الصق البصمة لترى كل ما تحمله، وأضف كلمة مرور لتحصل على نعم أو لا.

الأسئلة الشائعة

هل تُرسَل كلمة مروري إلى خادوم؟
لا. كل شيء يجري في لسان المتصفح هذا — تعمل البصمة في Web Worker على جهازك، ولا تدخل كلمة المرور العنوان أبدًا، ولا يُكتب شيء في تخزين المتصفح. افتح لوحة الشبكة عندك، واكتب كلمة مرور، واضغط الزرّ: لا يخرج أي طلب. هذا هو الفرق بين هذه الصفحة والموقعين المصنّفين فوقها على السؤال نفسه.
هل يمكن فكّ بصمة bcrypt رجوعًا إلى كلمة المرور؟
لا، وليس لأن ذلك صعب. كلمة المرور ليست في السلسلة: ما فيها نسخة وكلفة وملح وثلاثة وعشرون بايتًا من المخرجات، ولا يحتوي أيٌّ منها على المُدخل. وكل من يعرض فكّ واحدة منها إنما يخمّن كلمات مرور شائعة مقابل ملحك، وهذا بالذات ما صُمّم bcrypt خصّيصًا ليبطئه.
لماذا تعطي كلمة المرور نفسها بصمة مختلفة في كل مرة؟
لأن ملحًا عشوائيًّا جديدًا يُسحب لكل تشغيل، والملح جزء من السلسلة. وكل واحدة من تلك البصمات تطابق كلمة المرور نفسها — فالمدقّق يقرأ الملح من داخل البصمة التي تلصقها، فلا يحتاج أن يعرف أيّ تشغيل أنتجها.
قُطعت كلمة مروري عند اثنين وسبعين بايتًا. لماذا لم يظهر خطأ؟
لأنه ليس هناك ما يُكتشف. يخلط bcrypt في حالته اثنين وسبعين بايتًا من المفتاح بالضبط ولا يقرأ أبعد من ذلك أبدًا، فكلمة مرور طويلة وأول اثنين وسبعين بايتًا منها تنتجان الملخّص نفسه — والقطع لا يترك للمكتبة أي أثر تلحظه. وهذه الصفحة تقول لك إنه وقع، وهذا أقصى ما يستطيع تطبيق أن يفعله بصدق.
لماذا بلغت كلمة مروري الحدّ عند ستة وثلاثين حرفًا؟
لأن الحدّ اثنان وسبعون بايتًا لا اثنان وسبعون حرفًا، والحرف العربي أو العبري أو الروسي أو اليوناني يكلّف بايتين في UTF-8. واليابانية والكورية والصينية تكلّف ثلاثة، فيصل الحدّ هناك عند أربعة وعشرين حرفًا، ومعظم الإيموجي يكلّف أربعة. والعدّاد الذي تحت الحقل يعرض عدد البايتات أثناء الكتابة.
أيّ نسخة أختار؟
تلك التي تتوقّعها المنظومة التي تلصق فيها: ‎$2y$‎ لـPHP وLaravel، و‎$2a$‎ لـSpring Security القديم، و‎$2b$‎ في كل مكان آخر. وهذا قرار توافق لا قرار أمان — فعند أي طول يمكن أن تُعطاه هذه الأداة، تنتج الثلاث ملخّصات متطابقة ولا تختلف إلا في تلك الحروف الأربعة التي في المقدّمة.
أيّ كلفة أستخدم؟
12 هي الافتراضية الشائعة وأرضية إنتاج معقولة. استخدم كلفة منخفضة لتجهيزات الاختبار حتى لا تدفع حزمة اختباراتك ثمن مطّ المفتاح خمسين مرة، وقس كل ما فوق 15 مقابل حركة تسجيل الدخول الحقيقية عندك أولًا، لأن كل دخول ناجح يدفع الثمن نفسه الذي يدفعه المهاجم.
هل بصمة صُنعت هنا آمنة لوضعها في جدول مستخدمين حقيقي؟
لزرع قاعدة بيانات أو كتابة تجهيزة اختبار أو إضافة سطر htpasswd — نعم، فالملح يأتي من مصدر العشوائية المعمّى في متصفحك. لكن هذه صفحة مسوّدة لا خدمة اعتماد: إن كتبت الملح بنفسك كفّت النتيجة عن كونها آمنة للتخزين، وهو ما تقوله الصفحة حين يقع، وكلمة مرور تستحقّ الحماية الأولى بها أن تُسكّ حيث ستعيش.
لماذا تفتتح بصمتي بـ‎$2y$‎ والكود الذي أنشأها يقول bcrypt؟
لأن password_hash في PHP تكتب ‎$2y$‎، وLaravel مبنيّ عليها. وهي الخوارزمية نفسها التي في ‎$2b$‎؛ والنسخة تسجّل أيّ تطبيق كتب السلسلة. والمدقّق الذي يرفضها يرفض النسخة ولا يفشل في مطابقة كلمة المرور.
لصقت بصمة فقالت إن الملح كُتب بهجاء آخر. هل هي معطوبة؟
لا. اثنان وعشرون حرفًا تسع من البتات أكثر مما تحتاجه ستة عشر بايتًا، فتسقط أربع بتات في النهاية ويكون لبعض الأملاح أكثر من هجاء واحد يفكّ إلى البايتات نفسها. وقد أخرجت منظومات حقيقية أمثالها، فهذه الصفحة تقرأ بصمة كهذه بدل أن ترفضها، وتقول لك ما الهجاء القياسي.

أدوات ذات صلة