דילוג לתוכן
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

סיסמאות ו-hashing עם Argon2id

למה hashes מהירים נכשלים בסיסמאות, מה salt ו-work factor עושים, ואיך מחשבים ומאמתים Argon2id ב-Python עם argon2-cffi.

ביניים8 דקות קריאהמאת NotHoly

במה המאמר עוסק

מסדי סיסמאות דולפים יותר ממה שצוותים מתכננים. כשזה קורה, התוקף מקבל hashes ולא סיסמאות. כמה עבודה נדרשת כדי לעבור מהאחד לשני תלוי בשתי בחירות: הפונקציה שנבחרה וההגדרות שניתנו לה. המאמר מסביר למה אחסון סיסמאות צריך סוג אחר של hash, מה salt ו-work factor עושים, ולמה ההנחיות הנוכחיות מצביעות על Argon2id. במעבדה מחשבים ומאמתים סיסמה ב-Python, קוראים כל חלק ב-hash המקודד ומחשבים מחדש סיסמאות שמורות כשההגדרות משתנות.

צריך טרמינל ו-Python 3. המעבדה מתקינה חבילה אחת, argon2-cffi.

למה hash מהיר הוא הכלי הלא נכון

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

OWASP Password Storage Cheat Sheet אומר את זה ישירות. SHA-256 מהיר, ומהירות היא בדיוק מה שתוקף צריך כשהקלט הוא סיסמה, כלומר מחרוזת קצרה שאדם בחר.

אפשר למדוד את הפער על המחשב שלך. המדידה במעבדה מחשבת hash לסיסמה פעם עם SHA-256 ו-salt ופעם עם Argon2id, ומדפיסה את הקצב של כל אחת.

מה salt עושה

salt הוא ערך אקראי, שונה לכל סיסמה, שנשמר ליד ה-hash. יש לו שני תפקידים.

  • שני משתמשים עם אותה סיסמה מקבלים ערכים שמורים שונים. טבלה שדלפה לא מראה אילו חשבונות חולקים סיסמה.
  • הוא מנטרל טבלאות שחושבו מראש. בלי salt, תוקף מחשב פעם אחת hash לרשימת סיסמאות נפוצות ומחפש בה כל hash שדלף. עם salt ייחודי לכל משתמש, צריך לתקוף כל סיסמה בנפרד.

salt הוא לא סוד, והוא לא הופך סיסמה חלשה לחזקה. התפקיד שלו הוא למנוע מהתוקף שימוש חוזר בעבודה בין משתמשים. RFC 9106 ממליץ על salt של 128 ביט ומתיר 64 ביט כשהמקום צפוף. NIST SP 800-63B דורש לפחות 32 ביט. ברירת המחדל של argon2-cffi היא 16 בתים, כלומר 128 ביט.

מה work factor עושה

work factor הוא הגדרה שמייקרת כל ניחוש בכוונה. שיטות ישנות כמו PBKDF2 סופרות בעיקר איטרציות. ל-Argon2 יש שלושה פרמטרים.

  • עלות זיכרון (m): ה-RAM, ב-KiB, שכל hash משתמש בו.
  • עלות זמן (t): כמה מעברים הפונקציה עושה על הזיכרון הזה.
  • מקביליות (p): כמה נתיבים יכולים לרוץ בו־זמנית.

הזיכרון חשוב יותר מכול. ל-GPU יש אלפי ליבות קטנות, ולכל ליבה יש מעט זיכרון משלה. אם כל ניחוש צריך עשרות מגה־בייט, התוקף מריץ במקביל פחות ניחושים משהיה מריץ מול פונקציה שצריכה כמה קילו־בייט. התכונה הזאת נקראת memory-hardness.

ה-work factor הוא חוגה ולא קבוע. מעלים אותו ככל שהחומרה מתקדמת וככל שתקציב ההתחברות מאפשר. שומרים את ההגדרות עם כל hash, כדי שאפשר יהיה לזהות hashes ישנים ולשדרג אותם מאוחר יותר. השלבים האחרונים של המעבדה עושים את זה.

למה Argon2id

Argon2 ניצח ב-Password Hashing Competition בשנת 2015. יש לו שלושה וריאנטים.

  • Argon2d בוחר כתובות זיכרון לפי הנתונים. הוא עמיד היטב בפני התקפות GPU, אבל דפוס הגישה התלוי בנתונים יכול להדליף מידע על תזמון.
  • Argon2i משתמש בדפוס גישה שלא תלוי בקלט. זה מגן מפני דליפות תזמון, אבל מוותר על חלק מהעמידות בפני התקפות שמחליפות זיכרון בזמן.
  • Argon2id מריץ את הדפוס שלא תלוי בנתונים במחצית הראשונה של המעבר הראשון, ואז עובר לדפוס התלוי בנתונים.

Argon2id מכסה את שני החששות. RFC 9106 ממליץ עליו כווריאנט לשימוש כללי, ו-OWASP מציב אותו ראשון באפשרויות ה-hashing לסיסמאות.

RFC 9106 נותן שתי הגדרות מומלצות. הראשונה, למערכות עם זיכרון פנוי, היא 2 GiB, ‏1 מעבר ו-4 נתיבים. השנייה, לסביבות עם זיכרון מוגבל, היא 64 MiB, ‏3 מעברים ו-4 נתיבים. המינימום של OWASP הוא 19 MiB, ‏2 איטרציות ו-1 נתיב. OWASP מונה גם חלופות שמחליפות זיכרון במעברים, כמו 12 MiB עם 3 מעברים או 7 MiB עם 5 מעברים, כולן עם 1 נתיב. ברירות המחדל של argon2-cffi תואמות לסט השני של ה-RFC.

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

NIST ניגש לזה אחרת. SP 800-63B דורש שסיסמאות יקבלו salt ו-hash בשיטה מתאימה, ואומר שיש להשתמש בשיטה מאושרת מ-SP 800-132, שמכסה את PBKDF2. הוא לא מזכיר את Argon2id. אם תוכנית ציות דורשת אלגוריתמים ש-NIST אישר, כדאי לוודא מול המבקר לפני הבחירה ב-Argon2id.

איך ה-hash השמור עובד, שלב אחר שלב

כשספריית hashing שומרת סיסמה, קורים חמישה דברים.

  1. הספרייה מייצרת salt אקראי.
  2. היא מריצה Argon2id על הסיסמה, ה-salt ופרמטרי העלות.
  3. היא מקודדת את הווריאנט, הגרסה, הפרמטרים, ה-salt והפלט למחרוזת אחת.
  4. בהתחברות היא קוראת את המחרוזת, לוקחת ממנה את ה-salt והפרמטרים, מחשבת מחדש את הפלט ומשווה בין השניים.
  5. אם הפרמטרים במחרוזת ישנים יותר מההגדרות הנוכחיות, מחשבים hash מחדש אחרי התחברות מוצלחת.

בגלל שלב 3 לא צריך עמודת salt נפרדת. למחרוזת המקודדת יש הפורמט הזה.

text
$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
  • argon2id הוא הווריאנט.
  • v=19 היא גרסת Argon2, כלומר 1.3, בכתיב עשרוני. בהקסדצימלי זה 0x13.
  • m, ‏t וגם p הם הזיכרון ב-KiB, עלות הזמן והמקביליות.
  • <salt> וגם <hash> הם base64 בלי padding. ה-hash הוא הפלט שנגזר, 32 בתים כברירת מחדל.

המחרוזת מהמעבדה ארוכה 97 תווים. צריך עמודה שמחזיקה לפחות 255 תווים, או טיפוס text. לא מפצלים אותה לעמודות, אלא אם כתיבת parser משלך נראית לך כמו בילוי.

מעבדה מעשית

המעבדה משתמשת בסביבה וירטואלית מקומית ולא צריכה שירות רשת. הפלט בהמשך מגיע ממחשב Windows 11 אחד עם Python 3.12.10, ‏argon2-cffi 25.1.0 ו-argon2-cffi-bindings 26.1.0. במחשב הזה python3 לא היה ב-PATH של Git Bash, ולכן הפקודות רצו בשם python. אם python3 עובד אצלך, אפשר להשתמש בו. מחרוזות ה-hash אצלך יהיו שונות מאלה שמופיעות כאן, כי ה-salt אקראי.

1. יצירת הסביבה

bash
python3 -m venv .venv && . .venv/bin/activate
pip install argon2-cffi

ב-Windows מפעילים עם .venv\Scripts\activate ב-PowerShell או ב-cmd, או עם . .venv/Scripts/activate ב-Git Bash.

2. חישוב, אימות ובדיקת הפרמטרים

bash
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h); print(ph.verify(h,'correct horse battery')); print(ph.check_needs_rehash(h))"

הפלט שהתקבל:

text
$argon2id$v=19$m=65536,t=3,p=4$abVC6VF2EGJW1SCX/D6WBQ$XHKggONKypxBhdq7sWTbL/sMkglTAKu9y1FlL5p4dic
True
False

verify מחזיר True עבור הסיסמה הנכונה. check_needs_rehash מחזיר False כי ה-hash הזה נוצר עם ברירות המחדל הנוכחיות. ברירות המחדל הן עלות זמן של 3, עלות זיכרון של 65536 KiB ‏(64 MiB), מקביליות של 4, hash של 32 בתים, salt של 16 בתים והווריאנט ID.

3. זיהוי סיסמה שגויה

bash
python3 -c "from argon2 import PasswordHasher; from argon2.exceptions import VerifyMismatchError; ph=PasswordHasher(); h=ph.hash('a');
try: ph.verify(h,'b')
except VerifyMismatchError: print('mismatch detected')"

הפלט שהתקבל:

text
mismatch detected

סיסמה שגויה לא מחזירה False. verify זורק VerifyMismatchError, ולכן הבדיקה שייכת לבלוק except. מחרוזת שמורה פגומה זורקת חריג אחר, InvalidHashError. מטפלים בזה כבעיית נתונים בצד השרת, ולא נותנים לזה להגיע למסך של המשתמש.

4. אותה סיסמה, salts שונים

מחשבים hash לאותה סיסמה פעמיים.

bash
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"

הפלט שהתקבל:

text
$argon2id$v=19$m=65536,t=3,p=4$p8OU0plYOCO2/v70mjYTFA$FwhMW3ECJ0F/8rulKrJ158HjyNekURguaCIfoVk0i/o
$argon2id$v=19$m=65536,t=3,p=4$MTgAgrtBHJmc1eL2iKy8nA$h/7+8J6Cdb82XTBGWsLjbYBIh05934Yel4y6tyZ6NYg

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

5. קריאת החלקים של ה-hash המקודד

שומרים את זה בשם parts.py.

python
from argon2 import PasswordHasher

h = PasswordHasher().hash("correct horse battery")
print(h.split("$"))

מריצים עם python3 parts.py. הפלט שהתקבל:

text
['', 'argon2id', 'v=19', 'm=65536,t=3,p=4', 'waS+Ec4iQCNIuhRQbv2sWQ', 'yLioGsHJoVv+vTu2kRoYmXTLjbbjJSe2tPSExz3Sd5A']

האיבר הראשון הריק מגיע מהתו $ שבהתחלה. אחריו באים הווריאנט, הגרסה, הפרמטרים, ה-salt וה-hash. האימות לא צריך שום דבר אחר, ולכן עמודה אחת מספיקה.

6. חישוב מחדש כשהפרמטרים משתנים

הגדרות משתנות עם הזמן. הסקריפט הזה יוצר hash עם הגדרות ישנות וחלשות יותר ואז בודק אותו מול ברירות המחדל הנוכחיות. שומרים אותו בשם rehash.py.

python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

old = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
stored = old.hash('correct horse battery')
print('stored:', stored)

new = PasswordHasher()  # current defaults
print('needs rehash under new params:', new.check_needs_rehash(stored))
try:
    new.verify(stored, 'correct horse battery')
    print('verify ok')
    if new.check_needs_rehash(stored):
        stored = new.hash('correct horse battery')
        print('rehashed:', stored)
except VerifyMismatchError:
    print('wrong password')
print('after rehash needs rehash:', new.check_needs_rehash(stored))

מריצים python3 rehash.py. הפלט שהתקבל:

text
stored: $argon2id$v=19$m=19456,t=2,p=1$EWji+p6+3HbAKY+wkykcXg$NxPq/zGPsx9okK4vQCKfZ4BVoSOqeF1QkHxd3f2g2uE
needs rehash under new params: True
verify ok
rehashed: $argon2id$v=19$m=65536,t=3,p=4$FkifxeWUHV9eU7d8JFt6mg$ZFoMkNjYqo4D/bax/jPz80Hvr4mjLWRi4TVoqCoR6ng
after rehash needs rehash: False

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

7. מדידת העלות על החומרה שלך

שומרים את זה בשם bench.py.

python
import hashlib, time, os
from argon2 import PasswordHasher

pw = b'correct horse battery'
salt = os.urandom(16)
n = 1_000_000
t = time.perf_counter()
for _ in range(n):
    hashlib.sha256(salt + pw).digest()
dt = time.perf_counter() - t
print(f"sha256 salted: {n} hashes in {dt:.2f} s, {n/dt:,.0f} per second")

ph = PasswordHasher()
m = 10
t = time.perf_counter()
for _ in range(m):
    ph.hash('correct horse battery')
dt = time.perf_counter() - t
print(f"argon2id default: {m} hashes in {dt:.2f} s, {dt/m*1000:.0f} ms per hash")

מריצים python3 bench.py. הפלט שהתקבל:

text
sha256 salted: 1000000 hashes in 4.06 s, 246,343 per second
argon2id default: 10 hashes in 6.40 s, 640 ms per hash

אלה הרצות בודדות על מחשב Windows אחד. שורת ה-SHA-256 מודדת לולאת Python ולא קוד ממוטב, ולכן היא ממעיטה במה שחומרה ייעודית עושה עם SHA-256. שורת ה-Argon2id היא העלות שכל התחברות משלמת. המספרים המדויקים חשובים פחות מהפער. קריאת SHA-256 עם salt לוקחת כאן בערך 4 מיקרו־שניות. קריאת Argon2id לוקחת בערך 640 אלפיות שנייה, וככה זה תוכנן. מכווננים את m, ‏t וגם p על חומרה שדומה לפרודקשן, עד שהתחברות אחת עולה כמה מאות אלפיות שנייה.

טעויות נפוצות

  • hashes מהירים. שימוש ב-SHA-256, ב-SHA-512 או ב-MD5 לסיסמאות. סיסמה היא לא checksum של קובץ.
  • salts משותפים או צפויים. salt אחד לכל המשתמשים, או salt שנגזר משם המשתמש. לכל סיסמה מייצרים salt אקראי.
  • העתקת הגדרות בלי מדידה. ברירות המחדל של הספרייה הן התחלה הגיונית, ולא פסק דין עבור השרת שלך. קודם מודדים.
  • זיכרון בלי תקציב מקביליות. 64 MiB להתחברות נשמע מעט עד ש-100 התחברויות מגיעות יחד, וזה בערך 6.4 GB. מתכננים את הזיכרון לפי שיא ההתחברויות המקבילות.
  • השוואת מחרוזות hash בעצמך. קוראים לפונקציה verify. לא משווים מחרוזות עם ==.
  • סיסמאות בלוגים. לוגים, דוחות שגיאה ואנליטיקה לעולם לא צריכים להכיל את הסיסמה, את ה-hash או גוף מלא של בקשת התחברות.
  • אין מסלול שדרוג. שומרים את הפרמטרים בתוך המחרוזת השמורה ומחשבים מחדש בהתחברות מוצלחת.
  • התייחסות ל-pepper כפתרון. OWASP מציע pepper, סוד שנשמר מחוץ למסד הנתונים, כהגנה נוספת. ה-Cheat Sheet מציין גם ש-pepper לבדו מוסיף מעט. אם משתמשים בו, שומרים אותו במאגר סודות, ולא משתמשים בו במקום hash ראוי.

איך ממשיכים

  • לקרוא את סעיפי הפרמטרים והאבטחה ב-RFC 9106 ולהשוות אותם למודל האיומים שלך.
  • לקרוא את התיעוד של argon2-cffi עבור PasswordHasher, ‏verify וגם check_needs_rehash.
  • אם ל-framework שלך יש מודול סיסמאות, לבדוק אם הוא כבר משתמש ב-Argon2id לפני שכותבים wrapper.
  • לפני תכנון תהליך התחברות, כדאי להבין היטב את ההבדל בין hashing, קידוד והצפנה. המאמר הקודם מכסה את זה: הצפנה מול קידוד מול hashing. הבא עובר לזוגות מפתחות ולחתימות: מפתחות ציבוריים ופרטיים.

מקורות

מקורות

  1. RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications
  2. OWASP Cheat Sheet Series: Password Storage Cheat Sheet
  3. NIST SP 800-63B-4: Authentication and Authenticator Management
  4. argon2-cffi documentation

עוד בנושא קריפטוגרפיה