Tarkibga o'tish

Parollarni xeshlash (bcrypt)

Foydalanuvchi ro‘yxatdan o‘tayotganda parol kiritadi. Server bu parolni qandaydir tarzda saqlashi kerak, aks holda keyingi safar uni tekshirib bo‘lmaydi. Savol shundaki, qanday saqlash kerak?

Eng muhim qoida: parol hech qachon ochiq ko‘rinishda saqlanmaydi. Server faqat parolning qaytarib bo‘lmaydigan "izi"ni, ya’ni xeshini saqlaydi. Bu darsda to‘g‘ri xeshlash usulini va Go’da bcrypt bilan ishlashni o‘rganamiz.

Nima uchun bu muhim?

Ma’lumotlar bazasi o‘g‘irlanishi mumkin. Buning sababi SQL injection, eskirgan server, zaxira nusxaning noto‘g‘ri joyda qolishi yoki ichki xodim bo‘lishi mumkin. Bunday voqealar yirik kompaniyalarda ham bo‘lib turadi.

Baza o‘g‘irlanganda parollar qanday saqlanganiga qarab oqibat keskin farq qiladi:

Saqlash usuli Baza o‘g‘irlansa
Ochiq matn Barcha parollar darhol ochiladi
Shifrlangan Kalit ham topilsa, barcha parollar ochiladi
MD5 yoki SHA-256 Oddiy parollar bir necha soat, hatto daqiqalarda topiladi
bcrypt, argon2id Har bir parolni topish juda qimmat va sekin

Ko‘p odamlar bir xil parolni bir nechta saytda ishlatadi. Shu sababli sizning bazangizdan sizib chiqqan parol foydalanuvchining email yoki bank akkauntini ochishga ham xizmat qilishi mumkin.

Nima uchun oddiy xesh yetarli emas?

Xesh funksiya istalgan matnni qat’iy uzunlikdagi "barmoq izi"ga aylantiradi. Bu jarayonni teskari yo‘nalishda bajarib bo‘lmaydi. Xeshlash haqida umumiy ma’lumot Hashing darsida berilgan.

SHA-256 ham qaytarib bo‘lmaydigan xesh. Unda muammo nima?

1. U juda tez. SHA-256 ma’lumotni tez tekshirish uchun yaratilgan. Zamonaviy videokarta sekundiga milliardlab SHA-256 xeshini hisoblay oladi. Hujumchi mashhur parollar lug‘atidagi barcha so‘zlarni bir necha soniyada sinab chiqadi.

2. Bir xil parol — bir xil xesh. 123456 parolini ishlatgan barcha foydalanuvchilarning xeshi bir xil bo‘ladi. Hujumchi tayyor jadvallardan (rainbow tables) foydalanib, ularni bir zumda topadi.

SHA-256("123456") = 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92

Bu qatorni internet qidiruv tizimiga kiritsangiz, parol darhol topiladi.

bcrypt qanday hal qiladi?

bcrypt — parollar uchun maxsus ishlab chiqilgan xeshlash algoritmi. U yuqoridagi ikkala muammoni hal qiladi:

  1. Tuz (salt). Har bir parol uchun tasodifiy qiymat yaratiladi va parolga qo‘shiladi. Bir xil parol har safar boshqacha xesh beradi. Tayyor jadvallar ishlamaydi.
  2. Sozlanadigan sekinlik (cost). Algoritm ataylab sekin ishlaydi. Server uchun bitta parolni tekshirishga ketadigan 50–100 ms sezilmaydi. Milliardlab variantni sinab ko‘rmoqchi bo‘lgan hujumchi uchun esa bu yuzlab yillar degani.

bcrypt natijasi shunday ko‘rinadi:

$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
└┬┘└┬┘└─────────┬────────────┘└──────────────┬──────────────┘
 │  │         salt (22 belgi)             xesh (31 belgi)
 │  cost = 10
 algoritm versiyasi

Tuz va cost xeshning o‘zida saqlanadi. Shuning uchun ularni alohida ustunda saqlash shart emas. Bazada bitta TEXT ustun yetarli.

O‘rnatish

bcrypt Go jamoasi qo‘llab-quvvatlaydigan golang.org/x/crypto modulida joylashgan:

go get golang.org/x/crypto/bcrypt

Xeshlash va tekshirish

main.go
package main

import (
    "errors"
    "fmt"
    "log"

    "golang.org/x/crypto/bcrypt"
)

func main() {
    password := "juda-maxfiy-parol"

    // 1. Ro‘yxatdan o‘tishda: xeshni yaratish
    hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Xesh:", string(hash))

    // Bir xil parol — har safar boshqa xesh
    hash2, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    fmt.Println("Xesh:", string(hash2))

    // 2. Kirishda: parolni tekshirish
    err = bcrypt.CompareHashAndPassword(hash, []byte("juda-maxfiy-parol"))
    fmt.Println("To‘g‘ri parol:", err == nil)

    err = bcrypt.CompareHashAndPassword(hash, []byte("noto‘g‘ri-parol"))
    fmt.Println("Noto‘g‘ri parol:", errors.Is(err, bcrypt.ErrMismatchedHashAndPassword))
}
Xesh: $2a$10$zVQQA.RX8jdSZVsp91BX/.xubGQ4gVpxfxXUAsI.0JwWVjAS5Ph4m
Xesh: $2a$10$IVsXJdYPe/U/E.ZjxooZ3e9lFXsO/NQiDn41PHpDhVa6VwtDkqwRO
To‘g‘ri parol: true
Noto‘g‘ri parol: true

Xesh qiymatlari har safar boshqacha chiqadi, sizda ham boshqacha bo‘ladi.

Kod qanday ishladi:

  • GenerateFromPassword tasodifiy tuz yaratadi, parolni xeshlaydi va natijani yuqorida ko‘rilgan formatda qaytaradi.
  • CompareHashAndPassword xeshdan tuz va cost qiymatini oladi, kiritilgan parolni xuddi shu sozlamalar bilan xeshlaydi va natijalarni solishtiradi. Mos kelsa nil, aks holda ErrMismatchedHashAndPassword qaytaradi.
  • Ikkala xesh har xil bo‘lsa ham, ikkalasi ham to‘g‘ri parolni tasdiqlaydi.

Warning

Xeshlarni == yoki bytes.Equal bilan solishtirmang va parolni qayta xeshlab natijani solishtirishga urinmang. Tuz har safar yangi bo‘lgani uchun bu ishlamaydi. Tekshirish uchun faqat CompareHashAndPassword ishlatiladi.

Cost qiymatini tanlash

cost — sekinlik darajasi. Qiymat 1 ga oshsa, hisoblash vaqti ikki barobar ortadi.

Konstanta Qiymat
bcrypt.MinCost 4
bcrypt.DefaultCost 10
bcrypt.MaxCost 31

Cost qiymatining ta’sirini o‘z kompyuteringizda o‘lchab ko‘ring:

main.go
package main

import (
    "fmt"
    "time"

    "golang.org/x/crypto/bcrypt"
)

func main() {
    for cost := 10; cost <= 14; cost++ {
        start := time.Now()
        bcrypt.GenerateFromPassword([]byte("parol"), cost)
        fmt.Printf("cost=%d: %v\n", cost, time.Since(start).Round(time.Millisecond))
    }
}
cost=10: 51ms
cost=11: 96ms
cost=12: 191ms
cost=13: 387ms
cost=14: 765ms

Natija protsessorga bog‘liq. Qoida sifatida: server uskunangizda bitta xeshlash taxminan 100–250 ms oladigan eng katta cost qiymatini tanlang. Kamida 10 bo‘lishi kerak. Qiymatni juda katta qilmang: har bir login so‘rovi CPU’ni shuncha vaqt band qiladi. Bir vaqtda ko‘p login bo‘lsa, server sekinlashadi.

72 bayt cheklovi

bcrypt parolning faqat birinchi 72 baytini ishlatadi. golang.org/x/crypto/bcrypt paketi bundan uzun parolni jimgina qisqartirmaydi, balki bcrypt.ErrPasswordTooLong xatosini qaytaradi:

long := strings.Repeat("a", 73)
_, err := bcrypt.GenerateFromPassword([]byte(long), bcrypt.DefaultCost)
fmt.Println(err) // bcrypt: password length exceeds 72 bytes

Shuning uchun parol uzunligini validatsiya bosqichida cheklang: validate:"min=8,max=72". Validator belgilarni, bcrypt esa baytlarni sanaydi. Kirill harflari va ‘ kabi belgilar bir necha baytdan iborat. Aniqroq bo‘lish uchun baytlarni ham tekshiring:

if len(password) > 72 {
    return errors.New("parol juda uzun")
}

Go’da len() satrdagi baytlar sonini qaytaradi.

Real loyihada: password paketi

bcryptni butun kod bo‘ylab to‘g‘ridan-to‘g‘ri chaqirish o‘rniga, uni kichik paketga o‘rab qo‘yish qulay. Shunda cost qiymati va qoidalar bitta joyda turadi:

main.go
package main

import (
    "errors"
    "fmt"

    "golang.org/x/crypto/bcrypt"
)

const cost = 12

var (
    ErrTooShort = errors.New("parol kamida 8 belgidan iborat bo‘lishi kerak")
    ErrTooLong  = errors.New("parol 72 baytdan oshmasligi kerak")
)

// Hash parolni tekshiradi va xeshlaydi.
func Hash(plain string) (string, error) {
    if len([]rune(plain)) < 8 {
        return "", ErrTooShort
    }
    if len(plain) > 72 {
        return "", ErrTooLong
    }
    h, err := bcrypt.GenerateFromPassword([]byte(plain), cost)
    if err != nil {
        return "", fmt.Errorf("parolni xeshlash: %w", err)
    }
    return string(h), nil
}

// Verify parol xeshga mos kelishini tekshiradi.
// Ikkinchi qiymat xeshni yangi cost bilan qayta hisoblash kerakligini bildiradi.
func Verify(hash, plain string) (ok bool, needsRehash bool) {
    if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)); err != nil {
        return false, false
    }
    c, err := bcrypt.Cost([]byte(hash))
    return true, err == nil && c < cost
}

func main() {
    // Eski tizimda cost=10 bilan saqlangan xesh
    old, _ := bcrypt.GenerateFromPassword([]byte("eski-parol-123"), 10)

    ok, rehash := Verify(string(old), "eski-parol-123")
    fmt.Println("Parol to‘g‘ri:", ok, "| Qayta xeshlash kerak:", rehash)

    if rehash {
        newHash, _ := Hash("eski-parol-123")
        c, _ := bcrypt.Cost([]byte(newHash))
        fmt.Println("Yangi cost:", c)
    }

    _, err := Hash("123")
    fmt.Println(err)
}
Parol to‘g‘ri: true | Qayta xeshlash kerak: true
Yangi cost: 12
parol kamida 8 belgidan iborat bo‘lishi kerak

Cost’ni bosqichma-bosqich oshirish

Kompyuterlar yildan yilga tezlashadi, shuning uchun vaqt o‘tishi bilan cost qiymatini oshirish kerak bo‘ladi. Lekin eski xeshlarni qayta hisoblab bo‘lmaydi, chunki asl parollar ma’lum emas.

Yechim Verify funksiyasidagi needsRehash qiymatida. Foydalanuvchi tizimga kirganda parol to‘g‘ri bo‘lsa va xesh eski cost bilan yaratilgan bo‘lsa, parol yangi cost bilan qayta xeshlanadi va bazada yangilanadi. Faqat shu paytda asl parol serverda bo‘ladi. Shu tariqa foydalanuvchilar sezmagan holda barcha faol akkauntlar yangi sozlamaga o‘tadi.

Bazada saqlash

CREATE TABLE users (
    id            BIGSERIAL PRIMARY KEY,
    email         TEXT        NOT NULL UNIQUE,
    password_hash TEXT        NOT NULL,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

Ustun nomi password emas, password_hash. Bu kichik tafsilot, lekin kodni o‘qigan har bir dasturchiga ustunda ochiq parol emasligini aniq bildiradi.

Go modelida xesh hech qachon JSON’ga tushmasligi kerak:

type User struct {
    ID           int64  `json:"id"`
    Email        string `json:"email"`
    PasswordHash string `json:"-"`
}

Boshqa algoritmlar

bcrypt 1999-yildan beri ishlatiladi va hanuzgacha xavfsiz hisoblanadi. Yangi loyihalar uchun OWASP tashkiloti Argon2id algoritmini birinchi o‘rinda tavsiya qiladi. U hisoblash vaqtidan tashqari xotira sarfini ham sozlash imkonini beradi, bu esa videokartadagi hujumlarni qiyinlashtiradi. Go’da u golang.org/x/crypto/argon2 paketida mavjud.

Qaysi algoritmni tanlasangiz ham, asosiy qoidalar bir xil:

  • faqat parollar uchun mo‘ljallangan algoritmdan foydalaning: argon2id, bcrypt yoki scrypt;
  • MD5, SHA-1 va SHA-256’ni parollar uchun ishlatmang;
  • o‘zingiz xeshlash algoritmi o‘ylab topmang.

Tavsiyalar va parametrlar vaqt o‘tishi bilan yangilanib turadi. Loyihani boshlashdan oldin OWASP Password Storage Cheat Sheet hujjatining joriy versiyasini tekshiring.

Xulosa

  • Parol ochiq ko‘rinishda ham, oddiy xesh ko‘rinishida ham saqlanmaydi.
  • bcrypt har bir parolga tasodifiy tuz qo‘shadi va ataylab sekin ishlaydi.
  • Xeshlash GenerateFromPassword bilan, tekshirish faqat CompareHashAndPassword bilan bajariladi.
  • Cost qiymati server uskunasiga qarab tanlanadi: bitta xesh taxminan 100–250 ms. Kamida 10.
  • bcrypt 72 baytdan uzun parolni rad etadi. Uzunlikni oldindan tekshiring.
  • Cost’ni oshirganda eski xeshlar foydalanuvchi tizimga kirgan paytda yangilanadi.

Keyingi darsda ma’lumotlar bazasi paroli kabi maxfiy sozlamalarni koddan ajratib, .env va muhit o‘zgaruvchilari orqali boshqarishni o‘rganamiz.