Maqolalar 7 daqiqa 34 ko‘rish
Parollarni to‘g‘ri saqlash: hashni o‘z ko‘zingiz bilan sinang
Baza sizib chiqsa, foydalanuvchilaringizning paroli hujumchi qo‘lida nima bo‘ladi? Bu parolni qanday saqlaganingizga bog‘liq. SHA-256 nega yaramasligini, salt nima berishini va Argon2 nega tanlanishini kod bilan ko‘ramiz.
Mundarija
Har qanday loyihada bir kun kelib beriladigan savol shu: agar bazangiz sizib chiqsa, undagi parollar bilan hujumchi nima qila oladi? Javob esa sizib chiqishning o‘zida emas, parolni qanday saqlaganingizda yashiringan. To‘g‘ri saqlangan bo‘lsa, hujumchi qo‘lidagi ma'lumot deyarli befoyda. Noto‘g‘ri saqlangan bo‘lsa, u bir necha soatda minglab hisobga kiradi.
Farqni his qilish uchun bir necha kod parchasini o‘zingiz ishga tushirib ko‘rasiz. Python yetadi, faqat bitta kutubxona o‘rnatamiz.
Nega parolni ochiq saqlab bo‘lmaydi — buni hamma biladi#
Parolni bazaga shundoqligicha yozib qo‘yish mumkin emasligi hammaga ayon. Shuning uchun odatda hash ishlatiladi: parolni bir tomonlama funksiyadan o‘tkazasiz, natijada tushunarsiz satr chiqadi, va bazada o‘sha satr saqlanadi. Muammo shundaki, ko‘pchilik shu yerda to‘xtaydi va noto‘g‘ri hash tanlaydi.
SHA-256 nega yaramaydi#
Buni sinab ko‘ramiz. SHA-256 Python ichida tayyor keladi:
import hashlib
print(hashlib.sha256(b"parol123").hexdigest())
a203ba4e9c565ec07ecedc94a54194aa0b237cf65203aca0e1bd420c93f2cc28
Ikki marta ishga tushiring — natija bir xil chiqadi. Ana shu birinchi muammo: bir xil parol har doim bir xil hash beradi. Demak hujumchi eng ko‘p ishlatiladigan bir million parolning hashini oldindan hisoblab qo‘yishi va sizning bazangizdagilar bilan solishtirishi kifoya. Bunday tayyor jadvallar internetda bemalol topiladi.
Ikkinchi va kattaroq muammo — tezlik. SHA-256 ataylab tez ishlaydigan qilib yaratilgan, chunki u fayllar butunligini tekshirish kabi ishlar uchun mo‘ljallangan. Buni o‘lchab ko‘rdim: oddiy noutbukda, bitta CPU yadrosida Python sekundiga taxminan besh million SHA-256 hash hisoblaydi. Bu faqat Python va faqat bitta yadro; shu ish uchun maxsus yozilgan dastur zamonaviy video kartada sekundiga milliardlab hash sinab ko‘radi. Ya'ni sizib chiqqan bazadagi oddiy parollar bir necha soniyada ochiladi. Parol uchun tez funksiya — bu fazilat emas, aynan kamchilik.
Salt: bitta muammoni yopadi#
Birinchi muammoga — bir xil parol bir xil hash berishiga — yechim bor va u salt deyiladi. Salt — har foydalanuvchi uchun alohida yaratiladigan tasodifiy qiymat. Uni parolga qo‘shib, keyin hashlaymiz:
import hashlib
import os
salt = os.urandom(16) # har foydalanuvchiga alohida, tasodifiy
print(hashlib.sha256(salt + b"parol123").hexdigest())
Endi kodni ikki marta ishga tushiring — natija har safar boshqacha chiqadi, chunki salt har safar yangi. Shu bilan tayyor jadvallar ishdan chiqadi va bir xil parolli ikki foydalanuvchi bazada butunlay boshqacha ko‘rinadi. Salt sirli narsa emas, u parol yonida ochiq saqlanadi — uning vazifasi yashirish emas, har bir hashni noyob qilish.
Lekin salt tezlik muammosini yechmaydi. GPU baribir tez, faqat endi u har foydalanuvchi uchun alohida ishlashi kerak, xolos. Asosiy yechim boshqa tomonda.
Argon2: ataylab sekin funksiya#
Parol uchun maxsus yaratilgan funksiyalar bor va ular boshqa maqsad ko‘zlaydi: ataylab sekin va qimmat ishlash. Bugungi eng yaxshi tanlov — Argon2id. Uni sinab ko‘ramiz:
uv add argon2-cffi
from argon2 import PasswordHasher
ph = PasswordHasher() # standart parametrlar allaqachon aqlli tanlangan
hash_ = ph.hash("parol123") # salt avtomatik ichida yaratiladi
print(hash_)
# $argon2id$v=19$m=65536,t=3,p=4$...
Chiqqan satrga qarang: uning ichida algoritm nomi, parametrlar va salt — hammasi bor. Ya'ni siz salt haqida alohida qayg‘urishingiz shart emas, kutubxona buni o‘zi bajaradi va hashning ichiga joylaydi. Tekshirish ham sodda:
ph.verify(hash_, "parol123") # to'g'ri bo'lsa True qaytaradi
ph.verify(hash_, "boshqa") # noto'g'ri bo'lsa xato tashlaydi
True
VerifyMismatchError: The password does not match the supplied hash
Shu yerda bitta foydali qo‘shimcha bor: ph.check_needs_rehash(hash_).
Bugun kuchli hisoblangan parametrlar bir necha yildan keyin kuchsiz bo‘lib
qoladi, shuning uchun login paytida shu tekshiruvni qilib, kerak bo‘lsa
parolni yangi parametrlar bilan qayta hashlab qo‘yish odat tusiga kirgan.
Hozirgi hash uchun u False, eski, zaif parametrlar bilan yasalgan hash
uchun True qaytaradi.
Endi hash chaqiruvining qancha vaqt olayotganini payqagandirsiz — u
SHA-256 dan sezilarli sekin. Bu aynan biz istagan narsa. Yuqoridagi
o‘lchovni shu yerda takrorladim: bitta Argon2id hash taxminan 30
millisekund oldi, ya'ni sekundiga 35 tacha. O‘sha mashinada SHA-256 esa
sekundiga besh millionni uddalagan edi — farq yuz mingdan ortiq barobar.
(Aniq raqam kompyuterga bog‘liq, lekin tartib shunday qoladi.)
Foydalanuvchi uchun bu kechikish sezilmaydi: login bir marta bo‘ladi va o‘ttiz millisekund ko‘zga tashlanmaydi. Hujumchi uchun esa hisob butunlay o‘zgaradi — ilgari bir soatda sinab bo‘ladigan parollar ro‘yxati endi yillarni oladi. Argon2 ustiga xotira ham talab qiladi (standart sozlamada har hash uchun 64 MB), bu esa GPU hujumini yanada qimmatlashtiradi, chunki video kartadagi yuzlab yadroga yetadigan xotira topilmaydi.
Django'da bularning hech biri qo‘lda qilinmaydi#
Yaxshi xabar: agar Django ishlatsangiz, yuqoridagilarning hammasini u siz
uchun qiladi. User yaratganingizda parol avtomatik hashlanadi, tekshirish
ham o‘zi bo‘ladi. Sizga faqat bitta sozlama orqali Argon2'ni birinchi o‘ringa
qo‘yish qoladi:
# settings.py
PASSWORD_HASHERS = [
# Birinchisi — yangi parollar shu bilan saqlanadi
"django.contrib.auth.hashers.Argon2PasswordHasher",
# Qolganlari Django'ning standart ro'yxati: eski parollarni tekshirish
# uchun kerak, shuning uchun bittasini ham olib tashlamang.
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
"django.contrib.auth.hashers.ScryptPasswordHasher",
]
Buning uchun ham argon2-cffi o‘rnatilgan bo‘lishi kerak.
Ro‘yxatdagi eski hasherlar nega kerak
Loyihangizda allaqachon PBKDF2 bilan saqlangan parollar bo‘lsa, ularni birdan Argon2'ga ko‘chirib bo‘lmaydi — parolning ochiq matni sizda yo‘q. Django buni ayyorlik bilan hal qiladi: eski hasherlar ro‘yxatda qolsa, foydalanuvchi keyingi marta login qilganida uning paroli tekshiriladi va o‘sha zahoti yangi algoritm bilan qayta saqlanadi. Ya'ni parollar odamlar login qilgani sari asta-sekin Argon2'ga ko‘chib boradi.
Bu "ayyorlik" rostdan ishlaydimi — tekshirib ko‘rdim. PBKDF2 bilan saqlangan foydalanuvchining paroli bazada shunday turardi:
pbkdf2_sha256$1500000$tC7GFcOIGjb8y1y4wY...
O‘sha foydalanuvchi bir marta login qilgandan keyin, hech qanday migratsiya yozmasdan:
argon2$argon2id$v=19$m=102400,t=2,p=8$UV...
Yo‘l-yo‘lakay bir narsani payqagandirsiz: Django'ning parametrlari yuqorida
o‘zimiz ko‘rgan m=65536,t=3,p=4 dan boshqacha. Bu normal — Django o‘z
tanlovini qiladi va uni versiyadan versiyaga yangilab boradi. Bu ham
"kriptografiyani o‘zingiz o‘ylab topmang" degan qoidaning bir ko‘rinishi.
Bitta qatorni o‘chirib, hammani tashqarida qoldirish#
Yuqoridagi ro‘yxatga alohida to‘xtalishimning sababi bor, chunki bu yerda
juda jim kechadigan xato bor. PASSWORD_HASHERS ni yozganingizda siz
Django'ning standart ro‘yxatini butunlay almashtirasiz, ustiga
qo‘shmaysiz. Ya'ni ro‘yxatga kirmagan hasher bilan saqlangan parol endi
tekshirilmaydi.
Buni ham sinab ko‘rdim: scrypt bilan saqlangan parolni to‘liq ro‘yxat
bilan tekshirsangiz True keladi, o‘sha hasherni ro‘yxatdan olib
tashlasangiz — False. Xato ham, ogohlantirish ham chiqmaydi. Log'da
ko‘rinadigan yagona narsa — "parol noto‘g‘ri" degan oddiy login xatosi,
holbuki parol to‘g‘ri. Shuning uchun bu ro‘yxatdan hech qachon qator
o‘chirmang, faqat yangisini tepaga qo‘shing.
Amaliy xulosa#
Bir necha oddiy qoida butun mavzuni qamrab oladi. Kriptografiyani o‘zingiz o‘ylab topmang — bu yerda "aqlli" bo‘lish deyarli har doim xatoga olib keladi. Parol uchun oddiy hash emas, Argon2id (yoki bcrypt) ishlating. Salt haqida qayg‘urmang, munosib kutubxona uni o‘zi boshqaradi. Parol uzunligiga yuqori chegara qo‘ymang, pastki chegara esa kamida sakkiz-o‘n ikki belgi bo‘lsin. Va agar Django ishlatayotgan bo‘lsangiz, aslida sizga faqat yuqoridagi bir blok sozlamani yozib qo‘yish qoladi, xolos.