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$‎; הווריאנט מתעד איזה מימוש כתב את המחרוזת. בודק שמסרב לו מסרב לווריאנט ולא נכשל בהתאמת הסיסמה.
הדבקתי גיבוב והוא אומר שהמלח נכתב באיות אחר. הוא שבור?
לא. עשרים ושניים תווים יכולים לקודד יותר סיביות משישה עשר בתים צריכים, ולכן ארבע סיביות בסוף נופלות ולחלק מהמלחים יש יותר מאיות אחד שמתפענח לאותם בתים. מערכות אמיתיות פלטו כאלה, ולכן הדף הזה קורא גיבוב כזה במקום לסרב לו, ואומר לכם מהו האיות הקנוני.

כלים קשורים