Menu

password_hash() ו-password_verify() ב-PHP: שמירת סיסמאות

בצעו hash לסיסמה עם password_hash($password, PASSWORD_DEFAULT), שמרו את התוצאה בת 60 התווים, ובדקו התחברות עם password_verify($password, $hash). למדו למה ה-hash משתנה בכל פעם, cost, password_needs_rehash, Argon2id, ולמה md5 ו-sha1 שגויות לסיסמאות.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

שמרו סיסמאות עם password_hash($password, PASSWORD_DEFAULT) ובדקו התחברות עם password_verify($password, $hash), שמחזירה true או false. לעולם אל תשמרו את הסיסמה עצמה, ולעולם אל תשתמשו בשבילה ב-md5() או ב-sha1().

הריצו את זה פעמיים: ה-hash שונה בכל פעם, ושתי הגרסאות מאומתות. אותו דבר נכון לכל hash בעמוד הזה, ולכן הפלט שלכם לא יתאים לשל אף אחד אחר.

התחברות עם password_verify

התחברות בודקת את הסיסמה שנשלחה מול ה-hash שנשמר כשהחשבון נוצר. הבלוק הזה "רושם" משתמש אחד ואז מציג טופס התחברות; נסו את הסיסמה הנכונה (hunter2), ואז סיסמה שגויה.

הודעת השגיאה זהה לאימייל לא מוכר ולסיסמה שגויה. אמירה איזה מהם שגוי מגלה לתוקף לאילו אימיילים יש חשבונות. אחרי התחברות מוצלחת, אתר אמיתי שומר את מזהה המשתמש ב-session, כמו שמוצג בעמוד ה-sessions.

מה יש בתוך ה-hash

hash של bcrypt הוא מחרוזת אחת שמחזיקה את כל מה ש-password_verify() צריכה: את האלגוריתם, את ה-cost ואת ה-salt, ואחריהם את ה-hash עצמו. password_get_info() קוראת את החלקים הראשונים בחזרה.

כיוון שה-salt שמור בתוך ה-hash, צריך רק עמודה אחת במסד הנתונים, ושני משתמשים עם אותה סיסמה עדיין מקבלים hashes שונים. הגדירו את העמודה כ-VARCHAR(255): bcrypt הוא 60 תווים, אבל PASSWORD_DEFAULT רשאית לעבור לאלגוריתם עם פלט ארוך יותר.

בחירת ה-cost

ה-cost קובע כמה איטי כל hash בכוונה, וכל צעד למעלה מכפיל את העבודה. PHP 8.3 משתמשת ב-cost 10 ל-bcrypt (PHP 8.4 העלתה את ברירת המחדל ל-12). התחברות צריכה לקחת בערך 100 עד 300 אלפיות שנייה של hashing; מדדו את זה בשרת שלכם:

המספרים תלויים במכונה, אבל כל שורה צריכה להיות בערך פי שניים מהקודמת. גבוה יותר בטוח יותר מול פיצוח ואיטי יותר לכל התחברות, אז בחרו את ה-cost הגבוה ביותר שדף ההתחברות שלכם יכול להרשות לעצמו.

שדרוג hashes ישנים עם password_needs_rehash

כשמעלים את ה-cost או כש-PHP משנה את PASSWORD_DEFAULT, ה-hashes הקיימים ממשיכים לעבוד, ו-password_needs_rehash() אומרת לכם אילו מהם מיושנים. הרגע היחיד שבו הסיסמה הגלויה בידיים שלכם הוא ההתחברות, ולכן שם משדרגים:

הקידומת $2y$10$ הופכת ל-$2y$12$, ובהתחברות הבאה password_needs_rehash() מחזירה false.

לעולם אל תשתמשו ב-md5 או ב-sha1 לסיסמאות

md5() ו-sha1() הן פונקציות hash כלליות ומהירות בלי salt. מהירות היא בדיוק הדבר הלא נכון לסיסמאות: תוקף עם טבלה שדלפה יכול לנסות מיליארדי ניחושים בשנייה על GPU, ו-hash בלי salt של סיסמה נפוצה כבר נמצא בטבלאות חיפוש ציבוריות. השוו את המהירות:

בזמן ש-password_hash() יוצרת hash אחד, md5 יוצרת מאות אלפים, ולכן כל ניחוש של מפצח זול פי כך. חפשו את 5f4dcc3b5aa765d61d8327deb882cf99 ותמצאו מיד את המילה "password".

אם אתם יורשים טבלה של hashes ב-md5, אי אפשר להמיר אותם ישירות. עטפו אותם במקום זאת: שמרו password_hash($oldMd5) ואמתו עם password_verify(md5($input), $stored), ואז החליפו כל אחד ב-hash רגיל כשהמשתמש הזה מתחבר בפעם הבאה.

Argon2id ומגבלת 72 הבתים

PASSWORD_ARGON2ID משתמשת ביותר זיכרון לכל hash, וזה הופך פיצוח עם GPU ליקר הרבה יותר. היא זמינה כש-PHP נבנית עם libargon2 (רוב החבילות של Linux כוללות אותה); בדקו עם defined('PASSWORD_ARGON2ID').

השורה האחרונה מדפיסה bool(true): שתי סיסמאות שונות מתאימות כי bcrypt מתעלמת מכל מה שאחרי בית 72. סיסמאות אמיתיות מגיעות לשם לעיתים רחוקות, אבל משפטי סיסמה ביפנית או בכתבים מרובי בתים אחרים מגיעים ל-72 בתים בסביבות 24 תווים. אם אתם מאפשרים סיסמאות ארוכות מאוד, השתמשו ב-Argon2id, שאין לה מגבלה כזו. password_verify() עובדת עם כל אחד מהאלגוריתמים, ולכן אפשר לעבור עם password_needs_rehash() כמו למעלה.

שאלות נפוצות

למה password_hash נותנת תוצאה שונה בכל פעם?

כל קריאה מייצרת salt אקראי חדש ושומרת אותו בתוך ה-hash, ולכן אותה סיסמה נותנת מחרוזת שונה בכל פעם. זה מכוון: password_verify() קוראת את ה-salt בחזרה מתוך ה-hash השמור, ולכן היא עדיין מחזירה true לסיסמה הנכונה.

מה צריך להיות האורך של עמודת הסיסמה בשביל password_hash?

hash של bcrypt מ-PASSWORD_DEFAULT הוא היום 60 תווים, אבל אלגוריתם ברירת המחדל יכול להשתנות בגרסת PHP עתידית. השתמשו ב-VARCHAR(255) כדי ש-hash ארוך יותר ייכנס בלי מיגרציה.

למה password_verify תמיד מחזירה false?

הסיבות הרגילות הן hash קטוע (עמודת VARCHAR(50) חותכת את ה-hash בן 60 התווים), העברה של password_hash() חדש של משהו כארגומנט השני במקום ה-hash ששמרתם, השוואה מול hash שנוצר עם md5(), או רווחים עודפים בערך השמור. בדקו ש-strlen($hash) הוא 60 והעבירו את הסיסמה הגלויה כארגומנט הראשון.

אפשר לפענח ערך של password_hash ב-PHP?

לא. hash הוא חד כיווני: אין פונקציה שהופכת אותו בחזרה לסיסמה. כדי לבדוק סיסמה מבצעים hash לניסיון ומשווים, וזה מה ש-password_verify() עושה. לסיסמה שנשכחה, שלחו קישור לאיפוס במקום את הסיסמה הישנה.

להשתמש ב-PASSWORD_DEFAULT, ב-PASSWORD_BCRYPT או ב-PASSWORD_ARGON2ID?

PASSWORD_DEFAULT היא הבחירה הרגילה: היום היא bcrypt ו-PHP עשויה להעביר אותה לאלגוריתם חזק יותר בהמשך, ו-password_needs_rehash() מאפשרת לאמץ אותו בהדרגה. PASSWORD_ARGON2ID חזקה יותר מול פיצוח עם GPU, ואפשר להשתמש בה כשבנייה של PHP אצלכם כוללת אותה.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל