4-bo‘lim

Parollarni saqlash

Nima uchun SHA-256 parol uchun yaramaydi, tuz nima beradi va Argon2id parametrlari qanday tanlanadi.

🕑 16 daqiqa o‘qish 📄 1 120 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Noto'g'ri usullar
  2. Tuz (salt) nima beradi
  3. To'g'ri yechim: password_hash
  4. Argon2id va bcrypt
  5. Narx parametri
  6. Algoritmni yangilash
  7. bcrypt ning ikki tuzog'i
  8. Qalampir (pepper)
  9. Qo'shimcha himoya qatlamlari
  10. Xulosa

Parol xeshlari o'g'irlanadigan ma'lumotlar ro'yxatida birinchi o'rinda turadi. Ular qanday saqlanganini belgilaydi: hujumchi bir necha soatda hammasini ochadimi yoki umuman ocha olmaydimi.

Noto'g'ri usullar #

PHP
function qator(string $usul, string $natija, string $baho): void
{
    printf("%-16s %-34s %s%s", $usul, $natija, $baho, PHP_EOL);
}
PHP
$parol = 'Parol123';

qator('ochiq matn', $parol, 'HALOKATLI');
qator('base64', base64_encode($parol), 'HALOKATLI - kodlash');
qator('md5', md5($parol), 'JUDA YOMON');
qator('sha256', substr(hash('sha256', $parol), 0, 32) . '...', 'YOMON');
qator('sha256+tuz', substr(hash('sha256', 'tuz' . $parol), 0, 32) . '...',
    'YETARLI EMAS');
Natija
ochiq matn       Parol123                           HALOKATLI
base64           UGFyb2wxMjM=                       HALOKATLI - kodlash
md5              6a5be8fb198f94424ffeed351c18ddf6   JUDA YOMON
sha256           46fac07aae8ccfcfc7e52b540af49b9c... YOMON
sha256+tuz       ad338e3d820de0beccda2902e437e47f... YETARLI EMAS
Nima uchun sha256 ham yetarli emas

Uchta alohida muammo bor:

1. Juda tez. 3-bo'limda o'lchadik: oddiy noutbukda soniyasiga million atrofida. GPU da bu son yuz millionlarga chiqadi. Hujumchi lug'atdagi barcha parollarni sinab chiqishga bir necha soat sarflaydi.

2. Tuzsiz - oldindan hisoblangan jadval ishlaydi. Agar tuz bo'lmasa, sha256('Parol123') butun dunyoda bir xil. Hujumchi millionlab parolning xeshini oldindan hisoblab qo'yadi (rainbow table) va faqat qidiradi.

3. Bir xil parol - bir xil xesh. Bazadagi ikki foydalanuvchining xeshi bir xil bo'lsa, ularning paroli ham bir xil ekani ko'rinib turadi.

Uchinchi muammoni ko'rsatamiz:

PHP
$xesh1 = hash('sha256', 'Parol123');
$xesh2 = hash('sha256', 'Parol123');

echo 'Husanboy: ', substr($xesh1, 0, 24), '...', PHP_EOL;
echo 'Malika:   ', substr($xesh2, 0, 24), '...', PHP_EOL;
echo 'bir xilmi: ', $xesh1 === $xesh2 ? 'HA - parol ham bir xil' : 'yoq',
    PHP_EOL;
Natija
Husanboy: 46fac07aae8ccfcfc7e52b54...
Malika:   46fac07aae8ccfcfc7e52b54...
bir xilmi: HA - parol ham bir xil

Tuz (salt) nima beradi #

PHP
// Har foydalanuvchiga alohida tasodifiy tuz
$tuz1 = random_bytes(16);
$tuz2 = random_bytes(16);

$xesh1 = hash('sha256', $tuz1 . 'Parol123');
$xesh2 = hash('sha256', $tuz2 . 'Parol123');

echo 'parollar bir xil, xeshlar bir xilmi: ',
    $xesh1 === $xesh2 ? 'ha' : 'YOQ', PHP_EOL;
echo 'tuz uzunligi: ', strlen($tuz1), ' bayt', PHP_EOL;
echo 'tuz maxfiymi: yoq - u xesh bilan birga saqlanadi', PHP_EOL;
Natija
parollar bir xil, xeshlar bir xilmi: YOQ
tuz uzunligi: 16 bayt
tuz maxfiymi: yoq - u xesh bilan birga saqlanadi
Tuz maxfiy emas

Bu ko'pchilikni ajablantiradi: tuz maxfiy emas va xesh bilan yonma-yon saqlanadi.

Uning vazifasi boshqa:

Tuz beradiTuz bermaydi
Oldindan hisoblangan jadvalni foydasiz qiladiHujumni imkonsiz qilmaydi
Har parolni alohida sindirishga majbur qiladiBitta parolni himoyalamaydi
Bir xil parollarni yashiradiTezlikni sekinlashtirmaydi

Ya'ni tuz hujumni ko'paytiradi, lekin sekinlashtirmaydi. Million foydalanuvchi bo'lsa, hujumchi million marta ish qilishi kerak - ammo har biri baribir tez.

Shuning uchun tuz yetarli emas. Sekinlik ham kerak.

To'g'ri yechim: password_hash #

PHP
$parol = 'Parol123';

$xesh = password_hash($parol, PASSWORD_DEFAULT);

echo 'uzunlik:  ', strlen($xesh), ' belgi', PHP_EOL;
echo 'boshlanish: ', substr($xesh, 0, 7), PHP_EOL;
echo 'formati:  $algoritm$narx$tuz+xesh', PHP_EOL;

print_r(password_get_info($xesh));

// Har chaqiruvda BOSHQA xesh - chunki tuz tasodifiy
$ikkinchi = password_hash($parol, PASSWORD_DEFAULT);
echo 'ikki xesh bir xilmi: ', $xesh === $ikkinchi ? 'ha' : 'YOQ', PHP_EOL;

// Lekin ikkalasi ham tekshiruvdan o'tadi
var_dump(password_verify($parol, $xesh));
var_dump(password_verify($parol, $ikkinchi));
var_dump(password_verify('NotogriParol', $xesh));
Natija
uzunlik:  60 belgi
boshlanish: $2y$10$
formati:  $algoritm$narx$tuz+xesh
Array
(
    [algo] => 2y
    [algoName] => bcrypt
    [options] => Array
        (
            [cost] => 10
        )

)
ikki xesh bir xilmi: YOQ
bool(true)
bool(true)
bool(false)
password_hash sizning o'rningizga hamma ishni qiladi
VazifaKim bajaradi
Tasodifiy tuz yaratishpassword_hash
Tuzni xesh bilan saqlashpassword_hash (formatning bir qismi)
Algoritm va parametrlarni yozib qo'yishpassword_hash
Tekshirishda tuzni ajratib olishpassword_verify
Doimiy vaqtda solishtirishpassword_verify

Sizga faqat natijani satr ustuniga saqlash qoladi.

Ustun kengligi: VARCHAR(255). Hozirgi bcrypt 60 belgi, Argon2id ~97 belgi, lekin kelajakdagi algoritmlar uzunroq bo'lishi mumkin.

Argon2id va bcrypt #

PHP
$parol = 'Parol123';

$variantlar = [
    'bcrypt' => PASSWORD_BCRYPT,
    'argon2i' => PASSWORD_ARGON2I,
    'argon2id' => PASSWORD_ARGON2ID,
];

foreach ($variantlar as $nom => $algo) {
    $x = password_hash($parol, $algo);
    printf("%-9s %-3d belgi  %s%s", $nom, strlen($x),
        explode('$', $x)[1], PHP_EOL);
}
Natija
bcrypt    60  belgi  2y
argon2i   96  belgi  argon2i
argon2id  97  belgi  argon2id
Parol xeshining tuzilishi bcrypt: $2y$ 12$ 22 belgi tuz 31 belgi xesh algoritm narx tuz (maxfiy emas) xeshning o'zi argon2id: $argon2id$ v=19$m=65536,t=4,p=1$ tuz xesh xotira, takror, parallellik Nima uchun ataylab sekin Foydalanuvchi kunda bir marta kiradi - 100 ms uni bezovta qilmaydi. Hujumchi milliardlab urinish qiladi - 100 ms uni to'xtatadi. Nomutanosiblik shu yerda. Argon2id ning qo'shimcha ustunligi U xotira ham talab qiladi - GPU da minglab yadroni parallel ishlatish qimmatlashadi.
Tuz xeshning ichida saqlanadi - alohida ustun kerak emas

Narx parametri #

PHP
$parol = 'Parol123';

// Nazariy nisbat: har +1 cost ishni ikki barobar oshiradi
printf("%-8s %-12s %s%s", 'cost', 'nisbiy ish', 'ustun', PHP_EOL);
echo str_repeat('-', 40), PHP_EOL;

foreach ([4, 8, 10, 12, 14] as $narx) {
    $nisbat = 2 ** ($narx - 4);
    printf("%-8d %-12s %s%s", $narx, 'x' . $nisbat,
        str_repeat('#', min(40, $nisbat)), PHP_EOL);
}

// Amalda ham shunday: eng past va eng yuqori narxni o'lchaymiz
$olcha = static function (int $narx) use ($parol): float {
    $b = hrtime(true);
    password_hash($parol, PASSWORD_BCRYPT, ['cost' => $narx]);
    return (hrtime(true) - $b) / 1e6;
};

$past = $olcha(4);
$yuqori = $olcha(12);

echo PHP_EOL;
printf('cost=12 cost=4 dan sekinroqmi: %s%s',
    $yuqori > $past ? 'ha' : 'yoq', PHP_EOL);
printf('nisbat 100 martadan kopmi: %s%s',
    $yuqori / $past > 100 ? 'ha' : 'yoq', PHP_EOL);
Natija
cost     nisbiy ish   ustun
----------------------------------------
4        x1           #
8        x16          ################
10       x64          ########################################
12       x256         ########################################
14       x1024        ########################################

cost=12 cost=4 dan sekinroqmi: ha
nisbat 100 martadan kopmi: ha

Aniq millisekundlar mashinaga bog'liq, lekin nisbat har joyda bir xil: cost bir birlikka oshsa, ish ikki barobar ko'payadi. cost=4 dan cost=12 gacha - 256 marta.

Narxni qanday tanlash

Qoida: eng sekin qiymatni tanlang, foydalanuvchi sezmasa bas.

VaziyatTavsiya
Veb ilova, oddiy serverbcrypt cost 12, ~100-250 ms
Kuchli servercost 13-14
Mobil/IoT, zaif protsessorcost 10-11
Argon2idmemory_cost 64 MB, time_cost 4

Ikki muhim eslatma:

1. Narxni vaqt o'tishi bilan oshirib boring. Uskuna tezlashadi, 2015-yildagi cost=10 bugun kam.

2. DoS xavfini unutmang. cost=15 va soniyasiga 100 ta kirish urinishi serveringizni yiqitadi. Kirish sahifasiga tezlik chegarasi ham kerak.

Argon2id da memory_cost ayniqsa muhim: u GPU hujumini qimmatlashtiradi, chunki har parallel oqim uchun alohida xotira kerak bo'ladi.

Algoritmni yangilash #

PHP
// Eski xesh - past narx bilan yaratilgan
$eskiXesh = password_hash('Parol123', PASSWORD_BCRYPT, ['cost' => 4]);

$yangiSozlama = ['cost' => 12];

echo 'eski xesh narxi: ', password_get_info($eskiXesh)['options']['cost'],
    PHP_EOL;
echo 'yangilash kerakmi: ',
    password_needs_rehash($eskiXesh, PASSWORD_BCRYPT, $yangiSozlama)
        ? 'HA' : 'yoq', PHP_EOL;

// Kirish jarayonida - parol ochiq matnda MAVJUD bo'lgan yagona payt
function kirish(string $parol, string $saqlangan): string
{
    if (!password_verify($parol, $saqlangan)) {
        return 'RAD: parol notogri';
    }

    if (password_needs_rehash($saqlangan, PASSWORD_BCRYPT, ['cost' => 12])) {
        $yangi = password_hash($parol, PASSWORD_BCRYPT, ['cost' => 12]);
        return 'OK + xesh yangilandi (narx: '
            . password_get_info($yangi)['options']['cost'] . ')';
    }

    return 'OK';
}

echo kirish('Parol123', $eskiXesh), PHP_EOL;
echo kirish('Notogri', $eskiXesh), PHP_EOL;
Natija
eski xesh narxi: 4
yangilash kerakmi: HA
OK + xesh yangilandi (narx: 12)
RAD: parol notogri
Migratsiya faqat kirish paytida mumkin

Bazadagi barcha xeshlarni bir vaqtda yangilab bo'lmaydi - ochiq parol sizda yo'q.

Yagona payt - foydalanuvchi kirganda: shu daqiqada parol qo'lingizda bo'ladi.

Shuning uchun migratsiya asta-sekin boradi:

VaqtYangilangan foydalanuvchilar
1-haftaFaol foydalanuvchilar
1-oyKo'pchilik
1-yilDeyarli hammasi
Hech qachonTashlab ketilgan hisoblar

Oxirgi qatorga e'tibor bering: yillar davomida kirmagan hisoblarning xeshi eski qoladi. Ularni majburiy parol almashtirishga yo'naltirish yoki hisobni muzlatish mumkin.

bcrypt ning ikki tuzog'i #

PHP
// 1. bcrypt faqat birinchi 72 BAYTNI hisobga oladi
$uzun = str_repeat('a', 72);
$yanaUzun = $uzun . 'BUTUNLAY-BOSHQA-QISM';

$xesh = password_hash($uzun, PASSWORD_BCRYPT);

echo 'birinchi parol uzunligi: ', strlen($uzun), PHP_EOL;
echo 'ikkinchi parol uzunligi: ', strlen($yanaUzun), PHP_EOL;
echo 'ikkinchisi ham otadimi: ',
    password_verify($yanaUzun, $xesh) ? 'HA - XAVF' : 'yoq', PHP_EOL;

// 2. Argon2id da bunday chegara yo'q
$xesh2 = password_hash($uzun, PASSWORD_ARGON2ID);
echo 'argon2id da otadimi:   ',
    password_verify($yanaUzun, $xesh2) ? 'ha' : 'yoq', PHP_EOL;
Natija
birinchi parol uzunligi: 72
ikkinchi parol uzunligi: 92
ikkinchisi ham otadimi: HA - XAVF
argon2id da otadimi:   yoq
72 bayt chegarasi - amaliy oqibati

bcrypt uzun parolni kesib tashlaydi. Bu odatda muammo emas (72 bayt uzun parol), lekin ikkita holatda xavfli:

HolatNima uchun xavfli
Parolni oldin xeshlab, keyin bcrypt ga berishHex xesh 64 belgi, base64 - 44; chegaraga yaqin
Parol menejeri yaratgan uzun iboralar72 baytdan keyingisi e'tiborsiz
Kirill yoki emoji parolBelgi ikki-to'rt bayt - chegara tezroq keladi

Uchinchisi ayniqsa muhim: "Мойпарольочень..." - 36 kirill belgisi allaqachon 72 bayt.

Yechim:

YechimIzoh
Argon2id ishlatishChegara yo'q
Parolni avval HMAC bilan qisqartirishhash_hmac('sha256', $parol, $qalampir, true) - 32 bayt
Parol uzunligini 72 bayt bilan cheklashFoydalanuvchiga noqulay

Ikkinchi yechim keyingi bo'limda ko'radigan qalampir bilan birga qulay chiqadi.

Qalampir (pepper) #

PHP
// Qalampir - BAZADA EMAS, ilova sozlamasida saqlanadigan maxfiy kalit
$qalampir = hex2bin(str_repeat('7f', 32));   // real kodda: muhit o'zgaruvchisi

function qalampirliXesh(string $parol, string $qalampir): string
{
    // HMAC parolni 32 baytga keltiradi va qalampirni qo'shadi
    $tayyor = hash_hmac('sha256', $parol, $qalampir, true);

    return password_hash(base64_encode($tayyor), PASSWORD_ARGON2ID);
}

function qalampirliTekshir(string $parol, string $xesh, string $qalampir): bool
{
    $tayyor = hash_hmac('sha256', $parol, $qalampir, true);

    return password_verify(base64_encode($tayyor), $xesh);
}

$xesh = qalampirliXesh('Parol123', $qalampir);

var_dump(qalampirliTekshir('Parol123', $xesh, $qalampir));
var_dump(qalampirliTekshir('Notogri', $xesh, $qalampir));

// Boshqa qalampir bilan - o'tmaydi
$boshqa = hex2bin(str_repeat('80', 32));
var_dump(qalampirliTekshir('Parol123', $xesh, $boshqa));
Natija
bool(true)
bool(false)
bool(false)
Qalampir nima beradi va nima bermaydi
BeradiBermaydi
Faqat baza o'g'irlansa - xeshlar foydasizServer butunlay buzilsa - yordam bermaydi
Bir vaqtda ikki tizimni buzishni talab qiladiZaif parolni kuchaytirmaydi
base64_encode orqali 72 bayt muammosini yechadiSekinlik bermaydi

Qalampir qo'shimcha qatlam, asosiy himoya emas. Uni qo'shishdan oldin password_hash to'g'ri ishlatilganiga ishonch hosil qiling.

Eng katta amaliy muammo - qalampirni almashtirish. U o'zgarsa, barcha xeshlar yaroqsiz bo'ladi. Shuning uchun uni versiyalash kerak:

KOD
xesh_versiyasi | qalampir_id | xesh

Kalitlarni almashtirish 18-bo'limning mavzusi.

Qo'shimcha himoya qatlamlari #

QatlamNima beradi
Tezlik chegarasi (IP va hisob bo'yicha)Onlayn taxmin qilishni to'xtatadi
Ikki bosqichli tasdiqlashParol o'g'irlansa ham himoya qoladi
Ma'lum sizib chiqqan parollarni rad etishEng ko'p ishlatiladigan parollarni bloklaydi
Uzun parolga ruxsat berishUzunlik murakkablikdan muhimroq
Parolni jurnalga yozmaslikEng ko'p uchraydigan sizib chiqish sababi
PHP
// Parol siyosati: uzunlikka tayaning, murakkablikka emas
function parolYaroqlimi(string $parol): string
{
    $bayt = strlen($parol);

    return match (true) {
        $bayt < 12 => 'RAD: kamida 12 belgi kerak',
        $bayt > 4096 => 'RAD: juda uzun (DoS himoyasi)',
        in_array(strtolower($parol), ['parol123456', '123456789012'], true)
            => 'RAD: juda keng tarqalgan parol',
        default => 'QABUL',
    };
}

foreach (['qisqa', 'Parol123', 'parol123456',
          'togri-ot-batareya-mixi', str_repeat('a', 5000)] as $p) {
    printf('%-26s %s%s',
        strlen($p) > 26 ? substr($p, 0, 23) . '...' : $p,
        parolYaroqlimi($p), PHP_EOL);
}
Natija
qisqa                      RAD: kamida 12 belgi kerak
Parol123                   RAD: kamida 12 belgi kerak
parol123456                RAD: kamida 12 belgi kerak
togri-ot-batareya-mixi     QABUL
aaaaaaaaaaaaaaaaaaaaaaa... RAD: juda uzun (DoS himoyasi)
Amaliy topshiriq
  1. Bir xil parolni ikki marta sha256 bilan xeshlang - natijalar bir xilmi?
  2. Har biriga alohida tuz qo'shing va natijani solishtiring.
  3. Nima uchun tuz maxfiy bo'lishi shart emasligini tushuntiring.
  4. password_hash ni ikki marta chaqiring - xeshlar farq qildimi?
  5. Ikkala xeshni ham password_verify bilan tekshiring.
  6. cost ni 4 dan 12 gacha oshirib, vaqt qanday o'zgarishini kuzating.
  7. password_needs_rehash bilan yangilash oqimini yozing.
  8. 72 baytdan uzun parol bilan bcrypt tuzog'ini takrorlang.
  9. Xuddi shu sinovni Argon2id da bajaring.
  10. Qalampirli xeshlash funksiyasini yozing va noto'g'ri qalampir bilan sinang.

Xulosa #

  • Parolni ochiq matnda, base64 da yoki MD5 bilan saqlash - halokatli.
  • sha256 ham yaramaydi: u juda tez va tuzsiz.
  • Tuz oldindan hisoblangan jadvallarni foydasiz qiladi va bir xil parollarni yashiradi.
  • Lekin tuz hujumni sekinlashtirmaydi - u faqat ko'paytiradi.
  • password_hash tuz yaratish, saqlash va tekshirishni o'zi bajaradi.
  • Har chaqiruvda boshqa xesh chiqadi, lekin password_verify ikkalasini ham tanidi.
  • Narx (cost) har birlik oshganda vaqtni ikki barobar oshiradi.
  • Yangilash faqat kirish paytida mumkin - password_needs_rehash.
  • bcrypt faqat birinchi 72 baytni hisobga oladi; Argon2id da bunday chegara yo'q.
  • Qalampir - qo'shimcha qatlam: baza o'g'irlansa xeshlarni foydasiz qiladi.

Keyingi bo'limda xabar butunligi va HMAC bilan tanishamiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.