IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi4/31-dars30 daqiqa
Mundarija (42)

Parolni xavfsiz saqlash: argon2id, tuz, PHC satri va bcrypt tuzog'i

Qisqacha: Parol bazada hech qachon ochiq yoki tez hash (SHA-256, MD5) bilan saqlanmaydi — faqat sekin, tuzli hash bilan. 2026-yilda eng yaxshi tanlov — argon2id (OWASP: m=19 MiB, t=2, p=1), Node 24 da u o'rnatilgan: crypto.argon2. Natija PHC satri ko'rinishida saqlanadi: algoritm, parametrlar, tuz va hash bitta qatorda. Keyin parametrlarni oshirsangiz, eski hash'lar ham tekshiriladi va login paytida yangilanadi. bcrypt parolning faqat birinchi 72 baytini ko'radi — kanon buni o'lchab ko'rsatdi.

Bu darsda

  • Nega tez hash parol uchun yaramasligini rainbow table misolida ko'rasiz.
  • argon2id parametrlarini (m, t, p) va PHC satrini o'qiy olasiz.
  • bcrypt'ning 72 bayt tuzog'ini va Unicode normallashtirishni tushuntirasiz.
  • Login paytida qayta hash'lash (rehash on login) va xavfli parametrlardan himoyani yozasiz.
  • vazifalar-api ga src/auth/parol.ts modulini, migratsiyani va 6 ta testni qo'shasiz.

Oldin bilishingiz kerak: crypto 2: parolni hash'lash, shifrlash va imzo (tuz, scrypt, argon2 asoslari), crypto 1: tasodif, hash va HMAC, OWASP Top 10:2025 xaritasi, Prisma Migrate chuqur.

crypto 2 darsida parolni scrypt bilan hash qilib, tekshirishni yozgansiz va argon2id Node 24 da borligini ko'rgansiz. Bugun shu bilimni haqiqiy loyihaga olib kiramiz: vazifalar-api da foydalanuvchilar paroli paydo bo'ladi. Loyihaga kirganda yangi savollar chiqadi: qaysi parametrlar, qaysi format, eski hash'lar bilan nima qilinadi, bir vaqtda 16 kishi kirsa nima bo'ladi.

1. Nega bu kerak?

1.1 Baza sizib chiqadi

Oldingi darsda A04 Cryptographic Failures toifasini ko'rdik. Endi uning eng mashhur ko'rinishi: baza sizib chiqishi. Bu juda ko'p uchraydi — noto'g'ri zaxira nusxa, ochiq qolgan port, SQL injection, xodimning noutbuki. Savol "sizib chiqadimi?" emas, "sizib chiqqanda nima bo'ladi?".

Agar parollar ochiq yozilgan bo'lsa — hujumchi hammasini darhol o'qiydi. Undan yomoni: odamlar bir parolni ko'p joyda ishlatadi. Sizning kichik vazifalar saytingizdan chiqqan parol bilan hujumchi o'sha odamning pochtasiga, Telegram'iga, bank ilovasiga kirishga urinadi. Bu hujum credential stuffing deyiladi.

Maqsad: baza butunlay o'g'irlansa ham, parollarni tiklash juda qimmat bo'lsin.

1.2 Autentifikatsiya va avtorizatsiya

28-qismda bu ikki so'z doim yonma-yon keladi, ular 11-qismdan tanish:

  • Autentifikatsiya (authentication) — "siz kimsiz?" savoliga isbot: parol, kod, barmoq izi. Pasportni ko'rsatish.
  • Avtorizatsiya (authorization) — "sizga nima mumkin?": faqat o'z vazifalaringiz, admin sahifasi yo'q. Pasport ko'rilgandan keyin qaysi eshikka kirishingiz mumkinligi.

Parol — autentifikatsiyaning eng keng tarqalgan usuli. Bugun uning saqlash qismini, keyingi darsda esa ro'yxat va login oqimini quramiz.

2. Nega oddiy hash yetmaydi

2.1 Rainbow table

crypto 1 darsida SHA-256 ni o'rgangansiz: bir xil matn — doim bir xil hash. Fayl tekshirish uchun bu ajoyib. Parol uchun — halokat. Hujumchi mashhur parollarning hash'ini oldindan hisoblab qo'yadi va bazadagi hash'ni shunchaki jadvaldan qidiradi. Bunday tayyor jadvalni rainbow table deyishadi (aniqrog'i — uning soddalashtirilgan ko'rinishi).

js
async function sha256(matn) {
  const baytlar = new TextEncoder().encode(matn);
  const hash = await crypto.subtle.digest("SHA-256", baytlar);
  return Array.from(new Uint8Array(hash), (b) =>
    b.toString(16).padStart(2, "0")).join("");
}

// hujumchi oldindan tayyorlagan jadval: hash → parol
const MASHHUR = ["123456", "qwerty", "parol123", "Toshkent2026"];
const jadval = new Map();
for (const p of MASHHUR) jadval.set(await sha256(p), p);

// bazadan sizib chiqqan hash (tuzsiz)
const tuzsiz = await sha256("parol123");
console.log(tuzsiz.slice(0, 16), "→", jadval.get(tuzsiz));

// xuddi shu parol, lekin tasodifiy tuz bilan
const tuzli = await sha256("k3Jx9Qw2" + "parol123");
console.log(tuzli.slice(0, 16), "→", jadval.get(tuzli));

Konsolda:

text
a203ba4e9c565ec0 → parol123
357d9e3ccedfc125 → undefined

Birinchi qator: tuzsiz hash jadvalda bor — parol bir zumda topildi. Ikkinchi qator: parol o'sha, lekin oldiga tasodifiy tuz (salt) qo'shilgan. Hujumchining jadvali endi yaroqsiz — u har foydalanuvchi uchun alohida jadval tuzishi kerak. Hash'ning faqat boshini ko'rsatdik (16 belgi) — telefon ekraniga sig'sin.

2.2 Tuz yetarli emas: tezlik

Tuz tayyor jadvalni o'ldiradi. Lekin hujumchi baribir har foydalanuvchi uchun alohida "taxmin qilib ko'rish" mumkin: tuzni oladi (u hash yonida ochiq turadi) va millionlab parolni sinaydi. Bu yerda hal qiluvchi narsa — bitta urinish qancha turadi.

Kanon o'lchovi (Node 24.21.0, i5-12500H, 2026-10-27): oddiy noutbukning bitta yadrosi 1 soniyada 149 216 ta SHA-256 hash hisobladi. Videokartada bu milliardlab. 8 belgili kichik harfli parolni bunday tezlikda bir necha daqiqada topish mumkin.

Shuning uchun parol uchun ataylab sekin algoritm kerak. Sizga bitta login uchun 30 ms sezilmaydi. Hujumchiga esa har urinish 30 ms — milliard urinish o'nlab yil.

Tekshirib ko'ring: Tuz sir emas va hash yonida ochiq turadi. Unda u nimadan himoya qiladi?

Javob

Tuzning vazifasi — bir xil parollarga har xil hash berish. Natijada:

  1. Tayyor jadval (rainbow table) ishlamaydi.
  2. Hujumchi bitta hisob bilan hamma foydalanuvchini birdan sinay olmaydi — har biriga alohida ish.
  3. Bazaga qarab "kimning paroli bir xil" ekanini bilib bo'lmaydi.

Buning uchun tuz faqat noyob bo'lishi kerak, sir emas.

3. Sekin hash algoritmlari

3.1 Uchta oila

Parol uchun maxsus algoritmlar — parol hash funksiyalari (password hashing function). Ular ataylab sekin va ko'pincha ko'p xotira talab qiladi:

Algoritm Yil Kuchi Holati
argon2id 2015 vaqt + xotira, sozlanadi OWASP tavsiyasi №1
scrypt 2009 vaqt + xotira argon2id bo'lmasa
bcrypt 1999 faqat vaqt, 72 bayt chegarasi eski tizimlar uchun

Kanon hammasini bitta mashinada o'lchadi — 9 marta ketma-ket, mediana:

Bitta parol hash'i: vaqt (mediana, 9 marta)
  • crypto.argon2 OWASPm=19 MiB, t=2, p=132,7 ms
  • argon2 paketi OWASPnative addon30 ms
  • bcrypt cost 10native70,9 ms
  • bcryptjs cost 10sof JS116,5 ms
  • bcrypt cost 12native489,7 ms
  • scrypt N=2^17OWASP, 128 MiB626,1 ms

Manba: Kanon o'lchovi (IlmHamroh, 2026-10): Node 24.21.0, OpenSSL 3.5.8, i5-12500H, Windows 11, argon2 0.45.1, bcrypt 6.0.0, bcryptjs 3.0.3, 2026-10-27

Rasmni to'g'ri o'qing: argon2id eng tez chiqdi, lekin bu "eng zaif" degani emas. Uning kuchi — xotira: har urinish 19 MiB oladi. Videokartada minglab yadro bor, lekin har biriga 19 MiB berish qimmat. bcrypt esa xotirani deyarli ishlatmaydi va 1999-yildan beri unga moslashgan maxsus qurilmalar bor.

Node'ning o'z crypto.argon2 i bilan argon2 npm paketi orasidagi farq kichik: 32.7 va 30.0 ms. Paket native C++ addon (o'rnatishda kompilyatsiya yoki tayyor binar kerak), Node'niki esa bog'liqliksiz. Kanon shuning uchun Node'nikini tanladi.

3.2 argon2id parametrlari

argon2id uchta parametr bilan sozlanadi:

Harf Nomi Ma'nosi OWASP minimal
m memory xotira, KiB (1 KiB = 1024 bayt) 19 456 KiB (19 MiB)
t passes xotira bo'ylab necha marta o'tish 2
p parallelism parallel yo'laklar soni 1

OWASP Password Storage Cheat Sheet bir nechta teng kuchli kombinatsiya beradi: m=47104 (46 MiB) va t=1, m=19456 va t=2, m=12288 va t=3 va boshqalar. Ko'p xotira bo'lsa — kam o'tish yetadi. vazifalar-api uchun kanon ikkinchisini oldi: m=19456, t=2, p=1.

"Variant" haqida bir gap: argon2 ning uch turi bor — argon2d, argon2i, argon2id. Parol uchun faqat argon2id: u ikkalasining yaxshi tomonini birlashtiradi.

3.3 NIST nima deydi

NIST — AQSh standartlar instituti; uning SP 800-63B hujjati butun dunyoda parol siyosati uchun asos bo'lib xizmat qiladi. Joriy nashri — SP 800-63B-4 (2025-yil iyul, avvalgi SP 800-63B o'rniga). Parol saqlash bo'yicha uning talablari (pages.nist.gov, 2026-10-28 da o'qildi):

  • parol tuzlanadi va mos parol hash sxemasi bilan hash qilinadi;
  • tuz kamida 32 bit va hash bilan birga saqlanadi;
  • narx (cost) parametri tekshiruvchi serverning ishlashini buzmaydigan darajada iloji boricha yuqori;
  • qo'shimcha ravishda faqat server biladigan maxfiy kalit bilan kalitli hash yoki shifrlash bosqichi bo'lishi tavsiya etiladi (should) — bu "pepper", «Pepper: maxfiy qo'shimcha» bo'limida.

NIST aniq algoritm nomini aytmaydi — u o'zining boshqa hujjatlariga (SP 800-132) havola qiladi. Aniq parametrlar uchun dasturchilar OWASP ga tayanadi. Parol uzunligi va tarkibi haqidagi NIST talablarini keyingi darsda, ro'yxatdan o'tish formasi bilan ko'ramiz.

4. crypto.argon2 va PHC satri

4.1 Birinchi hash

Node 24 dagi crypto.argon2 callback bilan ishlaydi, uni promisify (util moduli) bilan Promise'ga aylantiramiz. Natija bir xil bo'lsin deb tuzni qat'iy qildik — faqat ko'rsatish uchun:

js
import { argon2 } from "node:crypto";
import { promisify } from "node:util";

const argon2P = promisify(argon2);
const OWASP = { memory: 19456, passes: 2, parallelism: 1 };

// tuz faqat namoyish uchun qat'iy — haqiqiy kodda randomBytes(16)
const tuz = Buffer.from("IlmHamroh-tuz-16");
const hash = await argon2P("argon2id", {
  message: "Bahor-choyxona-2026",
  nonce: tuz,
  tagLength: 32,
  ...OWASP,
});

const b64 = (b) => b.toString("base64").replace(/=+$/, "");
console.log(tuz.length, hash.length);
console.log(
  `$argon2id$v=19$m=${OWASP.memory},t=${OWASP.passes},` +
    `p=${OWASP.parallelism}$${b64(tuz)}$${b64(hash)}`,
);

Konsolda:

text
16 32
$argon2id$v=19$m=19456,t=2,p=1$SWxtSGFtcm9oLXR1ei0xNg$nQYohXu/htZb0ZzSu+3Q4bh0L9H32vfldWmUtbZZf+Q

Parametr nomlari Node hujjatidagidek: message — parol, nonce — tuz (kamida 8 bayt), memory — KiB, passes — o'tishlar, parallelism — yo'laklar, tagLength — natija uzunligi (baytda). Bu blok faqat Node'da ishlaydi — brauzerda argon2 yo'q.

Node hujjatiga ko'ra crypto.argon2 v24.7.0 da qo'shilgan va unga alohida "eksperimental" belgisi qo'yilmagan — butun node:crypto moduli kabi barqaror (Stability: 2). Node 22 da yoki 24.7 dan oldingi versiyada u yo'q — shuning uchun package.json da "engines": { "node": ">=24" } turibdi.

4.2 PHC satrini o'qish

Ikkinchi qator — PHC satri (Password Hashing Competition formati). Uni $ bo'yicha bo'laklaymiz:

Bo'lak Qiymat Ma'nosi
1 argon2id algoritm
2 v=19 argon2 versiyasi (0x13)
3 m=19456,t=2,p=1 parametrlar
4 SWxtSGFtcm9oLXR1ei0xNg tuz, base64 (= siz)
5 nQYohXu/… hash, base64

Bu format nega muhim: bir qatorda hamma kerakli narsa bor. Ikki yildan keyin m ni 64 MiB ga oshirsangiz ham, eski foydalanuvchining satrida m=19456 yozilgan — server uni eski parametr bilan tekshira oladi. Boshqa tilda yozilgan dastur ham shu satrni tushunadi.

Kanon buni sinadi: Node crypto.argon2 bilan yasalgan satrni argon2 npm paketi tekshirdi va aksincha:

text
node PHC: $argon2id$v=19$m=19456,t=2,p=1$UftpOZ56yZA4IW+75w8dpQ$<hash>
argon2.verify(node PHC, to'g'ri parol): true | noto'g'ri: false
pkg  PHC: $argon2id$v=19$m=19456,p=1,t=2$1BQNVWqSsvbdE66GsmG5oQ$<hash>
nodeArgon2Tekshir(pkg PHC): true | noto'g'ri: false
bir xil parol — ikki hash teng emas (tuz): true

E'tibor bering: paket parametrlarni m,p,t tartibida yozdi, biz esa m,t,p. Ikkalasi ham to'g'ri — o'quvchi dastur nomiga qaraydi, joyiga emas. Bizning parol.ts faqat o'z tartibini o'qiydi — u faqat o'zi yozgan satrlarni tekshiradi, bu yetarli.

5. bcrypt tuzog'i va Unicode

5.1 72 bayt

bcrypt parolning faqat birinchi 72 baytini hisoblaydi, qolganini jim tashlaydi. Kanon tajribasi:

text
bcrypt: 72×'a'+'BIRINCHI' hash'i bilan 72×'a'+'BOSHQA' tekshiruvi → true (73-baytdan keyingi qism e'tiborga olinmaydi)
bcrypt: 36 kirill harfi (72 bayt) + 'X' vs + 'Y' → true (73 bayt — 37 belgi ham chegaradan oshdi)
argon2: xuddi shu sinov → false

Birinchi qator: ikki har xil parol bitta hash'ga mos keldi. Ikkinchisi — o'zbek foydalanuvchisi uchun muhimroq: kirill harfi UTF-8 da 2 bayt oladi. 37 ta kirill harfi — allaqachon 74 bayt. "Uzun parol — xavfsiz parol" degan foydalanuvchi bcrypt bilan buning teskarisini oladi.

Belgilar va baytlar farqini o'zingiz ko'ring:

js
const enc = new TextEncoder();

const lotin = "a".repeat(72) + "BIRINCHI";
console.log(lotin.length, enc.encode(lotin).length);

const kirill = "ж".repeat(36) + "X";
console.log(kirill.length, enc.encode(kirill).length);

Konsolda:

text
80 80
37 73

length — belgilar (aniqrog'i UTF-16 birliklari), encode(...).length — baytlar. Lotin harfi 1 bayt, kirill — 2 bayt. bcrypt baytlarni sanaydi. argon2 da bunday chegara yo'q — kanon tajribasida u ikki parolni to'g'ri farqladi.

5.2 Bir harf — ikki xil yozilish

Yana bir nozik joy. Ў harfini kompyuter ikki xil saqlay oladi: bitta belgi (NFC) yoki У + yoy belgisi (NFD). Ekranda bir xil, baytlarda — har xil:

js
const enc = new TextEncoder();
const nfc = "Ў";
const nfd = nfc.normalize("NFD");

console.log(nfc === nfd);
console.log(enc.encode(nfc).length, enc.encode(nfd).length);
console.log(nfd.normalize("NFC") === nfc);

Konsolda:

text
false
2 4
true

Hash baytlardan hisoblanadi — demak bir parol ikki hash beradi. Foydalanuvchi ro'yxatdan telefonda o'tdi, keyin kompyuterdan kirmoqchi — klaviatura boshqa shaklda yubordi — "parol noto'g'ri". Kanon testi aynan shuni tekshiradi: NFD shakl false qaytaradi.

Yechim: hash'dan oldin parolni bitta shaklga keltirish — normalize("NFC"). NIST SP 800-63B-4 ham shuni tavsiya qiladi. Kanon bu ishni parol.ts da emas, ro'yxat va login sxemasida qiladi — keyingi darsda.

Tekshirib ko'ring: Nega trim() (boshi va oxiridagi bo'shliqlarni olib tashlash) parolga qo'llanmaydi, normalize esa qo'llanadi?

Javob

Bo'shliq — parolning haqiqiy belgisi; foydalanuvchi uni ataylab qo'ygan bo'lishi mumkin. trim parolni o'zgartiradi va uni zaiflashtiradi. normalize esa hech narsani o'zgartirmaydi — ekranda bir xil ko'rinadigan narsani baytlarda ham bir xil qiladi. Kanon testida "Bahor-choyxona-2026 " (oxirida bo'shliq) — boshqa parol.

6. Parametrlarni yangilash va xavfli satrlar

6.1 Rehash on login

Bugun m=19 MiB. Uch yildan keyin serverlar kuchliroq, siz m ni oshirmoqchisiz. Lekin bazadagi eski hash'larni qayta hisoblab bo'lmaydi — buning uchun ochiq parol kerak, u esa sizda yo'q.

Yechim: foydalanuvchi kirganda parol sizning qo'lingizda bo'ladi (u hozirgina yubordi). Shu paytda:

  1. eski parametrlar bilan tekshirasiz;
  2. to'g'ri bo'lsa va parametrlar eskirgan bo'lsa — yangi parametrlar bilan qayta hash qilib, bazaga yozasiz.

Buni rehash on login (kirishda qayta hash'lash) deyishadi. OWASP ham eng keng tarqalgan yo'l sifatida shuni tavsiya qiladi.

flowchart TD
  K[Login: email + parol] --> S[(Bazadan PHC satri)]
  S --> F{Format va chegaralar}
  F -->|buzuq| R[401]
  F --> T{argon2 tekshiruvi}
  T -->|noto'g'ri| R
  T -->|to'g'ri| E{Parametrlar eskimi?}
  E -->|ha| Y[Yangi hash yozish]
  E -->|yo'q| O[Kirish]
  Y --> O

Diagrammaga qarang: "Format va chegaralar" bosqichi — tekshiruvdan oldin. Nima uchun — keyingi bo'limda.

6.2 Bazadagi satr ham xavfli bo'lishi mumkin

Server bazadagi PHC satriga ishonib, unda yozilgan m bilan argon2 ni ishga tushiradi. Agar kimdir satrni m=4194304 ga o'zgartirsa (4 GiB!), har login serverga 4 GiB xotira ajratishni buyuradi. Bu — xotira tugashiga olib keladigan DoS hujumi. Bazaga yozish imkoni qanday paydo bo'ladi? SQL injection, admin panel xatosi, buzilgan migratsiya.

"Hech kimga ishonma" tamoyili bu yerda ham ishlaydi: o'z bazangizdagi satr ham tekshiriladi. Kanon PHC satrini qat'iy regex va chegaralar bilan o'qiydi:

js
const PHC = new RegExp(
  String.raw`^\$argon2id\$v=19\$m=(\d{1,7}),t=(\d{1,2}),` +
    String.raw`p=(\d{1,2})\$([A-Za-z0-9+/]{16,64})\$` +
    String.raw`([A-Za-z0-9+/]{32,128})$`,
);
const CHEGARA = { m: 262_144, t: 10, p: 8 };
const JORIY = { m: 19_456, t: 2, p: 1 };

function parametrlar(saqlangan) {
  const t = PHC.exec(saqlangan);
  if (t === null) return null;
  const prm = { m: Number(t[1]), t: Number(t[2]), p: Number(t[3]) };
  if (prm.m > CHEGARA.m || prm.t > CHEGARA.t || prm.p > CHEGARA.p) {
    return null;
  }
  return prm;
}

function eskirganmi(prm) {
  return prm.m !== JORIY.m || prm.t !== JORIY.t || prm.p !== JORIY.p;
}

const tuz = "SWxtSGFtcm9oLXR1ei0xNg";
const hash = "nQYohXu/htZb0ZzSu+3Q4bh0L9H32vfldWmUtbZZf+Q";
const SATRLAR = [
  `$argon2id$v=19$m=19456,t=2,p=1$${tuz}$${hash}`,
  `$argon2id$v=19$m=4096,t=1,p=1$${tuz}$${hash}`,
  `$argon2id$v=19$m=4194304,t=2,p=1$${tuz}$${hash}`,
  "$2b$10$abcdefghijklmnopqrstuuJ1Yyq0zqP2yXv8xQ3lQx4n1yQy9n6qG",
  "Bahor-choyxona-2026",
];

for (const s of SATRLAR) {
  const prm = parametrlar(s);
  if (prm === null) console.log("rad");
  else console.log(`m=${prm.m} eski: ${eskirganmi(prm)}`);
}

Konsolda:

text
m=19456 eski: false
m=4096 eski: true
rad
rad
rad

Beshta satr, besh xil holat:

  1. Joriy parametrlar — oddiy tekshiruv.
  2. Eski, kuchsizroq parametrlar — tekshiriladi va to'g'ri bo'lsa, qayta hash'lanadi.
  3. 4 GiB so'ragan satr — hisoblashga ham yetib bormaydi.
  4. bcrypt satri ($2b$) — format boshqa, rad.
  5. Ochiq parol — rad.

Regex'ni new RegExp va String.raw (RegExp asoslari va metodlari) bilan bo'laklarga bo'ldik — telefon ekraniga sig'sin. \d{1,7} — m ko'pi bilan 7 xonali, chegaradan oshganini esa son sifatida tekshiramiz (256 MiB = 262 144 KiB).

Muhim: noto'g'ri satr xato tashlamaydi, false qaytaradi. Aks holda bazadagi bitta buzuq qator login'ni 500 xatoga aylantirardi.

7. timingSafeEqual

Hisoblangan hash'ni saqlangan hash bilan qanday solishtirish kerak? === — xato. Oddiy solishtirish birinchi farqli baytda to'xtaydi: bir bayt mos kelsa — biroz sekinroq, ikki bayt — yana sekinroq. Bu farq nanosekundlarda, lekin minglab o'lchov bilan uni sezish mumkin. Bu timing attack (vaqt hujumi) — crypto 1 darsida ko'rgansiz.

timingSafeEqual hamma baytni doim oxirigacha solishtiradi. Lekin uning bitta talabi bor:

js
import { timingSafeEqual } from "node:crypto";

const a = Buffer.from("abc");
console.log(timingSafeEqual(a, Buffer.from("abc")));
console.log(timingSafeEqual(a, Buffer.from("abd")));
timingSafeEqual(a, Buffer.from("abcd"));

Konsolda:

text
true
false
RangeError: Input buffers must have the same byte length

"Kirish buferlari bir xil bayt uzunligida bo'lishi kerak". Uzunligi farq qilsa — xato tashlaydi. Shuning uchun kanon avval uzunlikni tekshiradi: h.length === kutilgan.length && timingSafeEqual(h, kutilgan). && birinchisi false bo'lsa ikkinchisini chaqirmaydi — xato chiqmaydi.

Parol hash'ida timing hujumi amalda kam xavfli (argon2 ning o'zi 30 ms davom etadi va natija tasodifiy tuzga bog'liq). Lekin qoida oddiy: sirni solishtirish — doim timingSafeEqual. Keyinroq token, HMAC imzo va API kalitda bu qoida haqiqatan muhim bo'ladi.

8. Pepper: maxfiy qo'shimcha

Tuz hash yonida turadi va baza bilan birga o'g'irlanadi. Agar hamma parolga yana bitta maxfiy qiymat qo'shilsa va u bazada emas, serverning sozlamasida (yoki maxsus kalit qurilmasida) saqlansa-chi? Baza o'g'irlansa ham, pepper'siz hujumchi birorta parolni sinay olmaydi. Bu qiymat pepper (inglizcha "qalampir" — tuzdan keyingi ziravor) deyiladi.

Node crypto.argon2 da buning uchun alohida parametr bor — secret. Hujjatda aynan shunday yozilgan: tuzga o'xshash qo'shimcha kirish, lekin u hash bilan birga saqlanmasligi kerak, parol hash'lashda u pepper deyiladi.

Pepper'ning narxi ham bor. OWASP ogohlantiradi: pepper o'zgartirilsa (masalan, u ham sizib chiqdi), eski pepper bilan hash qilingan hamma foydalanuvchi parolini qayta o'rnatishi kerak — chunki eski hash'larni yangi pepper bilan qayta hisoblab bo'lmaydi. Bundan tashqari, pepper yo'qolsa — hech kim tizimga kira olmaydi.

Kanon qarori: pepper vazifalar-api ga kiritilmadi. Sabablari — aylantirish (rotation) murakkab, pepper'ni saqlash uchun alohida sir boshqaruvi kerak (Sirlarni boshqarish), kichik loyiha uchun foydasi xavfidan kichik. NIST SP 800-63B-4 esa uni tavsiya qiladi (majburiy emas). Halol xulosa: katta tizimda, kalitlarni boshqarish infratuzilmasi bor joyda — pepper qo'shing; kursdagi loyihada — tushuncha sifatida bilib qo'ying.

9. Ko'p login birdaniga

9.1 Thread pool navbati

crypto.argon2 asinxron: hisob Node'ning libuv thread pool'ida bajariladi (libuv thread pool), asosiy oqim bo'sh qoladi. Lekin pool'da sukut bo'yicha atigi 4 ta ish oqimi bor. 16 kishi bir vaqtda kirsa nima bo'ladi? Kanon o'lchadi:

Parallel login: eng sekin so'rov (crypto.argon2, OWASP)
eng sekin so'rov, ms
26250116bir vaqtdagi loginlar, taUV_THREADPOOL_SIZE=4: 1 ta → 50 msUV_THREADPOOL_SIZE=4: 4 ta → 65 msUV_THREADPOOL_SIZE=4: 8 ta → 128 msUV_THREADPOOL_SIZE=4: 16 ta → 262 ms
Parallel login: eng sekin so'rov (crypto.argon2, OWASP)
bir vaqtdagi loginlarUV_THREADPOOL_SIZE=4
150
465
8128
16262

Manba: Kanon o'lchovi (IlmHamroh, 2026-10): Node 24.21.0, i5-12500H (16 mantiqiy yadro), Windows 11, 2026-10-27

4 tagacha — deyarli bir xil (50 → 65 ms): to'rtta oqim parallel ishlaydi. Keyin chiziq tikka ko'tariladi: 8 ta login — ikki navbat, 16 ta — to'rt navbat. 16-kishi o'z javobini 262 ms da oladi. Mashinada 16 yadro bo'lsa ham — pool 4 ta.

9.2 Event loop va xotira

Ikki o'lchov yana muhim:

text
crypto.argon2 (async) 8 parallel paytida event loop maks kechikish: 12.0 ms
bcryptjs.hashSync(cost 10) paytida event loop maks kechikish: 113.9 ms (sinxron — butun server to'xtaydi)
m=19456 (19 MiB): 16 parallel — RSS cho'qqisi +76 MiB
m=65536 (64 MiB): 16 parallel — RSS cho'qqisi +257 MiB
  • Asinxron argon2 server ishini deyarli to'xtatmaydi (12 ms). Sof JS dagi sinxron bcryptjs.hashSync esa 114 ms davomida butun serverni to'xtatadi — boshqa hech kimning so'rovi javob olmaydi (Event loop'ni bloklash). Serverda faqat asinxron variant.
  • Xotira: bir vaqtda 4 ta hash (pool hajmi) × m. 19 MiB da +76 MiB, 64 MiB da +257 MiB. m ni tanlashda serveringiz xotirasini ham hisoblang.

Bu raqamlar hujumchi uchun ham qiziq: login sahifasiga ko'p so'rov yuborib, serverni band qilish mumkin. Himoyasi — tezlik cheklovi va bir vaqtdagi hash'lar sonini cheklash; DoS va resurs tugashidan himoya darsida kanon aynan shu yerga semafor qo'yadi.

10. Bazada: faqat hash

10.1 Migratsiya va CHECK

Parol hash'ini ilova hisoblaydi, baza faqat tayyor satrni saqlaydi. Postgres'da pgcrypto kengaytmasining crypt() funksiyasi bilan bazaning o'zida hash qilish mumkin. Kanon buni ataylab qilmadi: u holda parol SQL so'rov ichida ochiq holda bazaga boradi va so'rov jurnaliga, pg_stat_statements ga yoki xato xabariga tushib qolishi mumkin.

Kanon migratsiyasi:

sql
ALTER TABLE foydalanuvchilar ADD COLUMN parol_hash text;

-- Faqat PHC satri ko'rinishida. Ochiq parol tasodifan yozilsa —
-- baza rad etadi
ALTER TABLE foydalanuvchilar
  ADD CONSTRAINT foydalanuvchilar_parol_hash_check
  CHECK (parol_hash IS NULL OR parol_hash ~
    ('^\$argon2id\$v=19\$m=\d+,t=\d+,p=\d+'
     || '\$[A-Za-z0-9+/]+\$[A-Za-z0-9+/]+$'));

Kanonda regex bitta uzun satr; telefon uchun uni ikkiga bo'lib, || (satrlarni ulash) bilan birlashtirdik — biz o'z bazamizda sinadik, cheklov xuddi shunday ishlaydi. Bu — chuqur himoyaning yana bir qatlami. Kodda xato bo'lib, kimdir parol_hash ga ochiq parolni yozmoqchi bo'lsa, baza qabul qilmaydi. NULL ruxsat etilgan — paroli yo'q hisoblar (keyinroq Google, Telegram yoki SMS orqali kirganlar).

10.2 O'z bazangizda sinang

Quyidagi so'rovlar kanon sxemasi (109, 110 migratsiyalari bilan) to'ldirilgan lokal bazada bajarildi (biz ishga tushirdik, PostgreSQL 17.6). Namuna foydalanuvchilarning hash'lari:

sql
SELECT id, email, left(parol_hash, 31) AS hash_boshi
FROM foydalanuvchilar
ORDER BY id
LIMIT 3;

Natija:

text
 id |        email        |           hash_boshi
----+---------------------+---------------------------------
  1 | aziz@example.com    | $argon2id$v=19$m=19456,t=2,p=1$
  2 | dilnoza@example.com | $argon2id$v=19$m=19456,t=2,p=1$
  3 | sardor@example.com  | $argon2id$v=19$m=19456,t=2,p=1$
(3 rows)

Ochiq parolni yozishga urinish:

sql
\set VERBOSITY terse
UPDATE foydalanuvchilar SET parol_hash = 'Bahor-choyxona-2026'
WHERE id = 1;

Natija:

text
ERROR:  new row for relation "foydalanuvchilar" violates check constraint "foydalanuvchilar_parol_hash_check"

"Yangi qator foydalanuvchilar_parol_hash_check cheklovini buzadi". \set VERBOSITY terse — xatoning qisqa shakli; usiz DETAIL qatorida butun qator chiqadi. bcrypt satri ham xuddi shu xato bilan rad etiladi — baza faqat argon2id ni taniydi.

Va bitta qiziq tafsilot:

sql
SELECT count(*) FILTER (WHERE parol_hash IS NULL) AS parolsiz,
       count(DISTINCT parol_hash) AS har_xil_hash
FROM foydalanuvchilar;

Natija:

text
 parolsiz | har_xil_hash
----------+--------------
        1 |            1
(1 row)

Bitta parolsiz hisob (Shahnoza — ataylab). Lekin har_xil_hash = 1: qolgan hamma foydalanuvchida bir xil satr. Namuna fayli (110-namuna-parol.sql) hammaga bitta demo parol Bahor-choyxona-2026 ni bitta tayyor hash bilan yozadi. Bu faqat lokal kurs bazasi uchun qulaylik. Haqiqiy ro'yxatda har foydalanuvchi parolHash() orqali o'z tasodifiy tuzini oladi va bu so'rov foydalanuvchilar soniga teng son qaytaradi. Agar prodakshn bazangizda bu raqam kichik chiqsa — tuz ishlamayapti.

11. Hujumchi nigohi

Jadvalda har hujum yonida himoya va uni tasdiqlaydigan test yoki o'lchov turibdi:

Hujum Himoya Test yoki o'lchov
rainbow table bilan qidirish 16 bayt tasodifiy tuz ikki hash teng emas
videokartada milliardlab taxmin argon2id, 19 MiB × 2 JORIY testi, grafik
72 baytdan keyin farqli parol argon2id bcrypt true, argon2 false
bazaga m=4194304 yozish regex + chegaralar rad, 200 ms dan tez
ochiq parol ustunga CHECK — faqat PHC UPDATE → ERROR
ko'p bir vaqtdagi login tezlik cheklovi, semafor 16 login → 262 ms
solishtirish vaqtini o'lchash timingSafeEqual uzunlik + bayt testi

Nega bu hujumlar ishlaydi — har bo'limda ko'rdik: tuz yo'q, hash tez, bcrypt chegarasi, server satrga ishonadi, === erta to'xtaydi.

Sizgan baza uchun real hisob: kanon mashinasida bitta argon2id ~33 ms. Hujumchi videokartada tezroq ishlaydi, lekin har urinishga 19 MiB xotira kerak. 10 million mashhur parolni bitta foydalanuvchiga sinash — yuzlab soat. Million foydalanuvchiga — har biriga alohida. Biz tiklashni imkonsiz qilmadik — juda qimmat qildik. Zaif parol (123456) baribir birinchi urinishlarda topiladi. Shuning uchun keyingi darsda zaif parollarni ro'yxatdan o'tishda rad etamiz.

12. Ko'p uchraydigan xatolar

Xato Nega xavfli To'g'risi
sha256(parol) sekundiga yuz minglab urinish argon2id
bitta umumiy tuz hamma uchun bir xil parollar bir xil hash har parolga randomBytes(16)
hash'ni === bilan solishtirish timing attack timingSafeEqual + uzunlik
argon2Sync / hashSync serverda event loop to'xtaydi asinxron variant
parametrlarni alohida ustunda saqlamaslik keyin oshirib bo'lmaydi PHC satri
buzuq satrda xato tashlash login 500 beradi { togri: false }
parolni trim() qilish foydalanuvchi parolini o'zgartiradi faqat normalize("NFC")
bcrypt'da uzun parolga ruxsat 72 baytdan keyingisi jim tashlanadi argon2id yoki uzunlik cheklovi

13. Mashqlar

Mashqlar kurs/mashqlar/28/04-parol/ da.

1-mashq (oson): to'g'ri yoki noto'g'ri

  1. Tuz sir bo'lishi kerak, shuning uchun uni alohida jadvalda saqlash kerak.
  2. Bir xil parolli ikki foydalanuvchining argon2id hash'lari bir xil bo'ladi.
  3. PHC satrida parametrlar bo'lgani uchun m ni oshirgandan keyin eski hash'lar ham tekshiriladi.
  4. crypto.argon2 bilan yasalgan hash'ni argon2 npm paketi tekshira oladi.
  5. Pepper'ni o'zgartirish oson: eski hash'larni yangi pepper bilan qayta hisoblash kifoya.
Yechim
  1. Noto'g'ri — tuz noyob bo'lishi kerak, sir emas; u hash yonida (PHC satri ichida) turadi.
  2. Noto'g'ri — har hash'ga yangi tasodifiy tuz; kanon testi a !== b ni tekshiradi.
  3. To'g'ri — va to'g'ri bo'lsa, qaytaHash: true bilan yangisi yoziladi.
  4. To'g'ri — kanon ikki tomonlama sinadi: true / false.
  5. Noto'g'ri — buning uchun ochiq parol kerak; foydalanuvchilar parolni qayta o'rnatishi kerak bo'ladi (OWASP).

2-mashq (o'rta): bcrypt kesadimi?

bcryptKesadimi(parol) funksiyasini yozing: parolning UTF-8 dagi uzunligi 72 baytdan oshsa true. To'rtta parolda sinang va har biri uchun belgilar soni, baytlar soni va natijani chiqaring: Bahor-choyxona-2026, 72 ta a, 73 ta a, 37 ta ў. Ishora: baytlar — new TextEncoder().encode(parol).length, takrorlash — "a".repeat(72) (Kesish, bo'lish va to'ldirish).

Yechim
js
const enc = new TextEncoder();

function bcryptKesadimi(parol) {
  return enc.encode(parol).length > 72;
}

const SINOV = [
  "Bahor-choyxona-2026",
  "a".repeat(72),
  "a".repeat(73),
  "ў".repeat(37),
];
for (const p of SINOV) {
  console.log(p.length, enc.encode(p).length, bcryptKesadimi(p));
}

Konsolda:

text
19 19 false
72 72 false
73 73 true
37 74 true

Oxirgi qator: 37 belgi — lotin uchun qisqa parol, lekin kirillda 74 bayt. bcrypt ishlatadigan eski tizimda shunday parolga ruxsat bersangiz, foydalanuvchi oxirgi harfni "himoya" deb o'ylaydi, aslida u hisobga olinmaydi.

3-mashq (qiyin): brauzerda PBKDF2

Brauzerda argon2 yo'q, lekin Web Crypto'da PBKDF2 bor — OWASP uni FIPS talab qilinganda tavsiya qiladi (SHA-256 bilan 600 000 iteratsiya). parolHash(parol) va parolTekshir(saqlangan, parol) ni yozing: format pbkdf2-sha256$600000$<tuz>$<hash> (tuz va hash — base64). Tuz — 16 bayt crypto.getRandomValues. Solishtirish — har baytni oxirigacha (timingSafeEqual o'rniga XOR bilan). Formati noto'g'ri satr — false. Ishora: crypto.subtle.importKey("raw", …, "PBKDF2", false, ["deriveBits"]) va deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations }, kalit, 256); base64 — btoa/atob (crypto 1).

Yechim
js
const ITERATSIYA = 600_000;
const enc = new TextEncoder();
const b64 = (baytlar) => btoa(String.fromCharCode(...baytlar));
const baytga = (matn) =>
  Uint8Array.from(atob(matn), (c) => c.charCodeAt(0));

async function hosilQil(parol, tuz, iteratsiya) {
  const kalit = await crypto.subtle.importKey(
    "raw", enc.encode(parol), "PBKDF2", false, ["deriveBits"],
  );
  const bitlar = await crypto.subtle.deriveBits(
    {
      name: "PBKDF2", hash: "SHA-256",
      salt: tuz, iterations: iteratsiya,
    },
    kalit,
    256,
  );
  return new Uint8Array(bitlar);
}

async function parolHash(parol) {
  const tuz = crypto.getRandomValues(new Uint8Array(16));
  const h = await hosilQil(parol, tuz, ITERATSIYA);
  return `pbkdf2-sha256$${ITERATSIYA}$${b64(tuz)}$${b64(h)}`;
}

async function parolTekshir(saqlangan, parol) {
  const [nom, n, tuz, hash] = saqlangan.split("$");
  if (nom !== "pbkdf2-sha256" || !tuz || !hash) return false;
  const h = await hosilQil(parol, baytga(tuz), Number(n));
  const kutilgan = baytga(hash);
  if (h.length !== kutilgan.length) return false;
  let farq = 0;
  for (let i = 0; i < h.length; i++) farq |= h[i] ^ kutilgan[i];
  return farq === 0;
}

const a = await parolHash("Bahor-choyxona-2026");
const b = await parolHash("Bahor-choyxona-2026");
console.log(a.split("$").slice(0, 2).join("$"));
console.log(a === b);
console.log(await parolTekshir(a, "Bahor-choyxona-2026"));
console.log(await parolTekshir(a, "bahor-choyxona-2026"));
console.log(await parolTekshir("Bahor-choyxona-2026", "x"));

Konsolda:

text
pbkdf2-sha256$600000
false
true
false
false

Tuz va hash har safar boshqa, shuning uchun to'liq satrni emas, faqat boshini chiqardik. farq |= h[i] ^ kutilgan[i] — XOR bir xil baytlarda 0 beradi; birorta farq bo'lsa farq noldan katta bo'lib qoladi. Sikl birinchi farqda to'xtamaydi — vaqt mos kelgan baytlar soniga bog'liq emas. Iteratsiya soni ham satr ichida — PHC g'oyasi: keyin oshirsangiz, eskilari ham tekshiriladi. PBKDF2 xotira talab qilmaydi — shuning uchun serverda argon2id afzal.

14. Loyiha qadami: parol.ts va migratsiya

14.1 Migratsiya va Prisma sxemasi

Yangi SQL fayl sql/109-parol.sql — «Bazada: faqat hash» bo'limidagi ALTER TABLE lar. 27-qism qoidasi bo'yicha xuddi shu SQL Prisma migratsiyasi bo'lib ham yoziladi: prisma/migrations/20261027100000_parol/migration.sql. schema.prisma dagi Foydalanuvchi modeliga bitta maydon:

prisma
  /// argon2id PHC satri; NULL — parolsiz hisob (OIDC, Telegram, OTP)
  parolHash          String?  @map("parol_hash")

Kanon izohidagi migratsiya raqamini olib tashladik. Namuna fayl sql/110-namuna-parol.sql — faqat lokal baza uchun, hammaga bitta demo parol:

sql
-- Hamma namuna hisobining paroli: Bahor-choyxona-2026.
-- Shahnozada parol yo'q — parolsiz hisob misoli.
UPDATE foydalanuvchilar
SET parol_hash = '$argon2id$v=19$m=19456,t=2,p=1$FuHr…UywpI'
WHERE email <> 'shahnoza@example.com';

Hash satrini qisqartirdik (…) — to'liq qiymatni kanon faylidan oling yoki parolHash("Bahor-choyxona-2026") bilan o'zingiz yasang. Bu parol faqat sizning kompyuteringizdagi kurs bazasi uchun. *-namuna-* fayllar prodakshnga bormaydi.

14.2 src/auth/parol.ts

Darsdagi hamma g'oya bitta modulda. Telefon uchun uzun qatorlar va izohlarni bo'ldik va qisqartirdik, kod ma'nosi kanondagidek (bo'lingan nusxa tip, lint va testlardan o'tdi — biz tekshirdik):

ts
// parol.ts — parolni xavfsiz saqlash: argon2id, Node 24 ning o'z
// crypto.argon2 (v24.7.0+) — bog'liqliksiz.
// Saqlash formati — PHC satri:
// $argon2id$v=19$m=19456,t=2,p=1$<tuz>$<hash>
// — parametrlar hash bilan birga (keyin oshirsak, eskilari ham
// tekshiriladi).
import {
  argon2 as argon2Cb,
  randomBytes,
  timingSafeEqual,
} from "node:crypto";
import { promisify } from "node:util";

const argon2 = promisify(argon2Cb);

export type Argon2Parametrlari = { m: number; t: number; p: number };

/** OWASP Password Storage Cheat Sheet (argon2id, minimal tavsiya).
 * Oshirilsa — eski hash'lar login paytida qayta hash'lanadi */
export const JORIY: Argon2Parametrlari = Object.freeze({
  m: 19_456,
  t: 2,
  p: 1,
});

const TUZ_BAYT = 16;
const HASH_BAYT = 32;
// PHC: base64 "=" siz. Parametrlar oralig'i — DoS himoyasi: bazadagi
// buzilgan satr serverni 4 GiB xotira ajratishga majburlamasin
const PHC = new RegExp(
  String.raw`^\$argon2id\$v=19\$m=(\d{1,7}),t=(\d{1,2}),` +
    String.raw`p=(\d{1,2})\$([A-Za-z0-9+/]{16,64})\$` +
    String.raw`([A-Za-z0-9+/]{32,128})$`,
);
const CHEGARA = { mMaks: 262_144, tMaks: 10, pMaks: 8 };

const b64 = (b: Buffer): string =>
  b.toString("base64").replace(/=+$/, "");

async function hisobla(
  parol: string,
  tuz: Buffer,
  prm: Argon2Parametrlari,
  uzunlik = HASH_BAYT,
): Promise<Buffer> {
  return argon2("argon2id", {
    message: parol,
    nonce: tuz,
    memory: prm.m,
    passes: prm.t,
    parallelism: prm.p,
    tagLength: uzunlik,
  });
}

export async function parolHash(
  parol: string,
  prm: Argon2Parametrlari = JORIY,
): Promise<string> {
  const tuz = randomBytes(TUZ_BAYT);
  const h = await hisobla(parol, tuz, prm);
  const sozlama =
    `m=${String(prm.m)},t=${String(prm.t)},p=${String(prm.p)}`;
  return `$argon2id$v=19$${sozlama}$${b64(tuz)}$${b64(h)}`;
}

export type TekshiruvNatijasi = {
  togri: boolean;
  qaytaHash: boolean;
};

/**
 * Parolni saqlangan PHC satri bilan solishtiradi. `qaytaHash` — parol
 * to'g'ri, lekin hash eski parametrlar bilan: login paytida yangisini
 * yozish kerak (rehash on login). Formati tanilmagan satr —
 * `togri: false` (xato tashlamaydi: bazadagi buzuq qator login'ni
 * 500 qilmasin).
 */
export async function parolTekshir(
  saqlangan: string,
  parol: string,
  joriy: Argon2Parametrlari = JORIY,
): Promise<TekshiruvNatijasi> {
  const t = PHC.exec(saqlangan);
  if (t === null) return { togri: false, qaytaHash: false };
  const [, m, tt, p, tuz, hash] = t;
  const prm = { m: Number(m), t: Number(tt), p: Number(p) };
  if (
    prm.m > CHEGARA.mMaks ||
    prm.t > CHEGARA.tMaks ||
    prm.p > CHEGARA.pMaks ||
    prm.m < 8 * prm.p
  ) {
    return { togri: false, qaytaHash: false };
  }
  const kutilgan = Buffer.from(hash ?? "", "base64");
  const h = await hisobla(
    parol,
    Buffer.from(tuz ?? "", "base64"),
    prm,
    kutilgan.length,
  );
  // timingSafeEqual — solishtirish vaqti mos kelgan baytlar soniga
  // bog'liq emas
  const togri =
    h.length === kutilgan.length && timingSafeEqual(h, kutilgan);
  const eski =
    prm.m !== joriy.m || prm.t !== joriy.t || prm.p !== joriy.p;
  return { togri, qaytaHash: togri && eski };
}

/**
 * Foydalanuvchi topilmaganda ham BIR XIL ish (argon2) bajariladi —
 * javob vaqti "bunday email yo'q" ni oshkor qilmasin (timing).
 * Ishga tushishda bir marta hisoblanadi.
 */
let soxta: Promise<string> | undefined;
export function soxtaHash(): Promise<string> {
  soxta ??= parolHash(randomBytes(32).toString("base64"));
  return soxta;
}

Uch narsaga e'tibor bering:

  • prm.m < 8 * prm.p — Node hujjatiga ko'ra memory kamida 8 × parallelism bo'lishi shart; aks holda argon2 xato tashlardi. Tekshiruv uni oldindan ushlaydi va false qaytaradi.
  • hash ?? "" — TypeScript regex guruhini string | undefined deb biladi (noUncheckedIndexedAccess), shuning uchun zaxira qiymat.
  • soxtaHash() — hozircha ishlatilmaydi. U keyingi darsda kerak: email bazada bo'lmasa ham server argon2 ishini bajaradi. Aks holda javob tezligidan "bunday email yo'q" ekanini bilib olish mumkin. ??= — o'zgaruvchi bo'sh bo'lsa, qiymat beradi; natijada hash bir marta hisoblanadi.

14.3 Testlar

test/parol.test.ts — oltita test. Ulardan ikkitasi (telefon uchun bo'lingan):

ts
test("eski parametrlar bilan hash — to'g'ri, lekin " +
  "qaytaHash: true (rehash on login)", async () => {
  const eski = await parolHash(PAROL, { m: 4096, t: 1, p: 1 });
  assert.deepEqual(await parolTekshir(eski, PAROL),
    { togri: true, qaytaHash: true });
  assert.deepEqual(await parolTekshir(eski, "x"),
    { togri: false, qaytaHash: false });
  assert.deepEqual(JORIY, { m: 19_456, t: 2, p: 1 });
});

test("buzuq yoki xavfli satr — xato emas, togri: false " +
  "(bcrypt, ochiq parol, juda katta m — DoS)", async () => {
  const yaxshi = await parolHash(PAROL);
  // 4 GiB so'rasa — hisoblamaymiz
  const katta = yaxshi.replace("m=19456", "m=4194304");
  const SATRLAR = [
    "", PAROL,
    "$2b$10$abcdefghijklmnopqrstuuJ1Yyq0zqP2yXv8xQ3lQx4n1yQy9n6qG",
    katta, yaxshi.replace("argon2id", "argon2i"), `${yaxshi}x!`,
  ];
  for (const s of SATRLAR) {
    const b = performance.now();
    assert.deepEqual(await parolTekshir(s, PAROL),
      { togri: false, qaytaHash: false }, s);
    assert.ok(performance.now() - b < 200);
  }
});

Ikkinchi testdagi performance.now() - b < 200 — "rad etish tez bo'lsin" degan talab. 4 GiB lik satr hisoblashga yetib borsa, test bu chegarani buzardi. Qolgan to'rt test: format va har xil tuz, registr va bo'shliq farqi, NFC/NFD, soxtaHash.

Faqat shu fayl testlarini ishga tushiramiz (biz ishga tushirdik, kanon nusxasi va yuqoridagi bo'lingan parol.ts bilan, Node 24.21.0):

bash
node --test test/parol.test.ts
text
✔ parolHash — PHC satri, joriy parametrlar, har safar boshqa tuz (59.2439ms)
✔ parolTekshir — to'g'ri / noto'g'ri / registr va bo'shliq farq qiladi (180.0863ms)
✔ Unicode: o'zbekcha va kirill parol (NFC/NFD farqi — normallashtirish chaqiruvchida) (89.0655ms)
✔ eski parametrlar bilan hash — to'g'ri, lekin qaytaHash: true (rehash on login) (11.1955ms)
✔ buzuq yoki xavfli satr — xato emas, togri: false (bcrypt, ochiq parol, juda katta m — DoS) (29.6909ms)
✔ soxtaHash — bir marta hisoblanadi, hech qaysi parolga mos kelmaydi (57.9309ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 569.8217

Millisekundlar har ishga tushishda boshqacha. To'rtinchi testning qisqaligiga qarang (11 ms): u m=4096 bilan — 5 marta kam xotira, shuning uchun tezroq.

14.4 Tekshiruv va commit

Butun loyiha tekshiruvi (kanon, 2026-10-27, PostgreSQL 18.6): tip va lint xatosiz, testlar 118 (117 + 1 o'tkazilgan) — oldingi 112 ga oltita yangi test qo'shildi. Prisma migratsiyasi sql/ bilan bir xil, schema.prisma va migratsiyalar orasida farq yo'q ("This is an empty migration").

bash
M="feat(auth): parol hash — argon2id (Node crypto.argon2,"
M+=" PHC satri), rehash bayrog'i; db: 109 parol_hash"
git add src/auth/parol.ts test/parol.test.ts sql prisma
git commit -m "$M"

Xabarda apostrof (bayrog'i) bor, shuning uchun bu safar qo'sh tirnoq: bitta tirnoq ichida apostrof satrni yopib qo'yardi.

Bu darsda hali hech kim ro'yxatdan o'tmaydi va kirmaydi — API'da endpoint yo'q. Biz faqat poydevor qo'ydik: parol qanday saqlanishi va tekshirilishi.

15. Real ishda

  • Parol sizib chiqishlari muntazam bo'ladi. Tadqiqotchilar birinchi qaraydigan narsa — qanday hash ishlatilgani. MD5 yoki tuzsiz SHA-1 bo'lsa — kompaniya obro'si qattiq zarar ko'radi.
  • Freymvorklar (framework) bu ishni o'zi qiladi (rasmiy hujjatlarga ko'ra, 2026-10 holatiga). Django sukut bo'yicha PBKDF2 (SHA-256) ishlatadi va parametr o'zgarsa, foydalanuvchi kirganda hash'ni o'zi yangilaydi. Laravel sukut bo'yicha bcrypt ishlatadi, eskirgan hash'ni Hash::needsRehash aniqlaydi. Spring Security'ning DelegatingPasswordEncoder i sukut bo'yicha bcrypt'ga topshiradi va algoritmni keyin almashtirishga joy qoldiradi. Node'da bu ishni o'zingiz yozasiz yoki auth kutubxonasiga topshirasiz — Auth kutubxonalari va xizmatlari.
  • Eski tizimdan ko'chish: bazada MD5 bo'lsa, OWASP tavsiyasi — eski hash'ni yangi algoritm ichiga o'rash va foydalanuvchi kirganda to'g'ridan-to'g'ri argon2id ga almashtirish.
  • Intervyuda: "Parolni qanday saqlaysiz?", "Tuz va pepper farqi?", "Nega bcrypt emas, argon2id?", "Parametrlarni qanday yangilaysiz?".

Xulosa

  • Parol — faqat sekin, tuzli hash: argon2id (OWASP: m=19 MiB, t=2, p=1); Node 24 da crypto.argon2 (v24.7.0+).
  • Tuz — noyob, sir emas; rainbow table'ni o'ldiradi. Pepper — maxfiy, NIST tavsiya qiladi, lekin aylantirish qimmat.
  • PHC satri algoritm, parametr, tuz va hash'ni birga saqlaydi — eski hash'lar kirishda qayta hash'lanadi.
  • bcrypt faqat 72 baytni ko'radi (kirillda 36 harf); Unicode — NFC ga keltiriladi.
  • Bazadagi satr ham tekshiriladi: regex, parametr chegaralari, false (xato emas), timingSafeEqual.
  • Bazada CHECK faqat PHC satrini qabul qiladi; parol hash'i ilovada hisoblanadi, bazada emas.

Keyingi dars: Ro'yxat va login oqimi hamda hisob xavfsizligi — parolHash va parolTekshir endpoint'larga ulanadi. U yerda NIST bo'yicha parol siyosati, "email yoki parol noto'g'ri" degan bir xil javob va javob vaqtidan himoyani yozamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Parolni xavfsiz saqlash: argon2id, tuz, PHC satri va bcrypt tuzog'i — IlmHamroh