Asosiy kontentga o‘tish

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.

security python

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:

Python
import hashlib

print(hashlib.sha256(b"parol123").hexdigest())
Natija
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:

Python
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:

Terminal
uv add argon2-cffi
Python
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:

Python
ph.verify(hash_, "parol123")  # to'g'ri bo'lsa True qaytaradi
ph.verify(hash_, "boshqa")  # noto'g'ri bo'lsa xato tashlaydi
Natija
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:

Python
# 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:

Natija
pbkdf2_sha256$1500000$tC7GFcOIGjb8y1y4wY...

O‘sha foydalanuvchi bir marta login qilgandan keyin, hech qanday migratsiya yozmasdan:

Natija
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.

7 daqiqa 34 marta o‘qildi