असुरक्षित पासवर्ड हैशिंग (CWE-916): SHA-256 काफ़ी क्यों नहीं और पासवर्ड कैसे सहेजें
पासवर्ड को बिना salt वाले SHA-256, SHA-1 या MD5 digests के रूप में सहेजने का मतलब है कि लीक हुई table को GPU पर हर सेकंड अरबों अनुमानों से परखा जा सकता है, और एक जैसे पासवर्ड वाले सभी यूज़र्स का hash एक ही होता है। salts, Argon2id जैसे memory-hard functions, login पर rehash और dummy verification चुराए गए database को कैसे बचाते हैं, Python और argon2-cffi के उदाहरण के साथ।
आसान भाषा में (ELI5)
एक क्लोकरूम हर टोकन की एक प्रति रखता है ताकि जाँच सके कि कौन-सा कोट किसका है। अगर प्रतियाँ सादी फ़ोटोकॉपी हों, तो फ़ोल्डर चुराने वाला चोर सारे कोट लेकर निकल जाता है। तेज़ hash हल्की धुँधली फ़ोटोकॉपी जैसा है: तेज़ स्कैनर वाला चोर हर सेकंड लाखों को साफ़ कर लेता है। धीमा, ज़्यादा memory खाने वाला hash एक छोटी तिजोरी में बंद प्रति जैसा है जिसे खोलने में पूरा एक सेकंड और एक बड़ी वर्कबेंच लगती है, हर टोकन की अलग तिजोरी और हर एक का अलग ताला। मालिक दरवाज़े पर एक तिजोरी खोलता है और देरी महसूस नहीं करता; चोर को लाखों खोलनी पड़ती हैं।
इस पेज के मुख्य शब्द
- तेज़ hash
- SHA-256, SHA-1 और MD5 तेज़ होने के लिए बनाए गए हैं। एक आधुनिक GPU हर सेकंड अरबों SHA-256 hashes निकालता है, इसलिए छोटे या आम पासवर्ड brute force और dictionary हमलों में कुछ ही घंटों में टूट जाते हैं।
- Salt
- हर hash के साथ सहेजा गया एक random मान, ताकि एक जैसे पासवर्ड के hashes अलग-अलग हों। इसके बिना एक ही पहले से बनी table सारे खाते एक साथ तोड़ देती है, और एक जैसे hashes बता देते हैं कि कौन-से यूज़र एक ही पासवर्ड इस्तेमाल करते हैं।
- Memory-hard function (Argon2id)
- Argon2id हर अनुमान पर समय और memory दोनों ख़र्च करवाता है, जिससे GPU का ज़्यादातर फ़ायदा ख़त्म हो जाता है। OWASP का न्यूनतम मानक 19 MiB memory, दो iterations और एक lane है (
memory_cost=19456,time_cost=2,parallelism=1); scrypt और bcrypt भी स्वीकार्य विकल्प हैं। - PHC string और rehash
PasswordHasher.hash()$argon2id$v=19$m=19456,t=2,p=1$...जैसी string लौटाता है जिसमें algorithm, parameters, salt और hash होते हैं।check_needs_rehash()बताता है कि सहेजे गए parameters मौजूदा से कमज़ोर हैं, ताकि अगले सफल login पर hash अपग्रेड हो सके।- समय और खातों की पहचान
- अगर अनजान emails का जवाब तुरंत आए और असली emails में hash जाँच जितना समय लगे, तो जवाब का समय बता देता है कि कौन-से खाते मौजूद हैं। dummy hash से जाँच करने पर दोनों रास्तों में बराबर समय लगता है।
हमले का चरण-दर-चरण प्रवाह
यूज़र्स की table लीक होती है
SQL injection, खुला छोड़ा गया backup या चोरी हुआ लैपटॉप हमलावर को password_hash column दे देता है।
हमलावर GPU cracker चलाता है
बिना salt का SHA-256 उसे एक wordlist का hash एक बार निकालकर हर row से मिलाने देता है। Summer2024! पासवर्ड वाला एक टेस्ट खाता दिखाता है कि आम पैटर्न कितनी जल्दी टूटता है।
एक जैसे पासवर्ड साथ टूटते हैं
एक ही पासवर्ड चुनने वाले हर यूज़र का digest एक जैसा होता है, इसलिए एक hash टूटते ही सब उजागर हो जाते हैं।
पासवर्ड दूसरी जगहों पर दोबारा इस्तेमाल होते हैं
टूटे पासवर्ड email, बैंक और क्लाउड खातों पर आज़माए जाते हैं। 2012 में LinkedIn से लगभग 65 लाख बिना salt वाले SHA-1 password hashes लीक हुए, और 2016 में उसी घटना के लगभग 11.7 करोड़ credentials बिक्री के लिए सामने आए।
सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन
# users.py: हर पासवर्ड का एक तेज़, बिना salt वाला digest
import hashlib
from .db import db
# एक जैसा पासवर्ड, एक जैसा hash, GPU पर हर सेकंड अरबों अनुमान
def create_user(email: str, password: str) -> None:
digest = hashlib.sha256(password.encode()).hexdigest()
db.execute("INSERT INTO users (email, password_hash) VALUES (?, ?)", (email, digest))
# अनजान emails तुरंत लौटते हैं, इसलिए समय बताता है कि कौन-से खाते हैं
def check_login(email: str, password: str) -> bool:
row = db.execute("SELECT password_hash FROM users WHERE email = ?", (email,)).fetchone()
return row is not None and row[0] == hashlib.sha256(password.encode()).hexdigest()
# users.py: हर पासवर्ड के अपने salt के साथ Argon2id और login पर अपग्रेड
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError
from .db import db
# Argon2id के लिए OWASP न्यूनतम: 19 MiB, 2 iterations, 1 lane
hasher = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
DUMMY_HASH = hasher.hash("timing-equaliser")
# सहेजी गई string में algorithm, parameters, salt और hash होते हैं
def create_user(email: str, password: str) -> None:
if not 12 <= len(password) <= 128:
raise ValueError("password must be 12 to 128 characters")
db.execute("INSERT INTO users (email, password_hash) VALUES (?, ?)", (email, hasher.hash(password)))
# अनजान यूज़र्स भी एक जाँच का समय लेते हैं; कमज़ोर hashes अपग्रेड होते हैं
def check_login(email: str, password: str) -> bool:
row = db.execute("SELECT id, password_hash FROM users WHERE email = ?", (email,)).fetchone()
stored = row[1] if row else DUMMY_HASH
try:
hasher.verify(stored, password)
except (VerificationError, InvalidHashError):
return False
if row is None:
return False
if hasher.check_needs_rehash(stored):
db.execute("UPDATE users SET password_hash = ? WHERE id = ?", (hasher.hash(password), row[0]))
return True
इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट
- पासवर्ड को Argon2id (कम से कम 19 MiB memory, 2 iterations, 1 lane), scrypt या bcrypt से hash करें; password variables पर
hashlibcalls को चिह्नित करने वाला Semgrep नियम जोड़ें और MD5 व SHA-1 के लिए Bandit B324 चलाएँ। - library जो पूरी PHC string लौटाए वही सहेजें, और salt को कभी अलग न करें या दोबारा इस्तेमाल न करें।
- हर सफल login के बाद
check_needs_rehash()बुलाएँ और कमज़ोर parameters से बने hashes बदल दें। - खाता न होने पर भी dummy hash से जाँच करें, ताकि जवाब का समय पंजीकृत emails न बताए।
- कम से कम 12 अक्षर ज़रूरी करें, कम से कम 64 अक्षर तक लंबे passphrases की अनुमति दें, और breach सूचियों में मिले पासवर्ड ठुकराएँ।