2. Хэширование паролей
Почему нельзя хранить пароли в открытом виде?
Заголовок раздела «Почему нельзя хранить пароли в открытом виде?»Если база данных утечёт, все пароли сразу доступны злоумышленнику. И пользователи часто используют один пароль на многих сайтах.
// НИКОГДА так не делай!const user = { password: 'mysecretpassword' // открытый текст — катастрофа};Что такое хэш?
Заголовок раздела «Что такое хэш?»Хэш — это результат односторонней функции. Из хэша нельзя получить исходные данные обратно.
"password123" → bcrypt → "$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"Свойства хорошей хэш-функции для паролей:
- Однонаправленная: из хэша нельзя восстановить пароль
- Медленная: нельзя перебрать миллиарды вариантов
- Уникальная (с солью): одинаковые пароли дают разные хэши
Что такое соль (salt)?
Заголовок раздела «Что такое соль (salt)?»Соль — случайная строка, добавляемая к паролю перед хэшированием.
// Без соли — одинаковые пароли = одинаковые хэшиhash("password") → "abc123"hash("password") → "abc123" // легко увидеть совпадение
// С солью — разные хэши даже для одинаковых паролейhash("password" + "xK9p2mN1") → "def456"hash("password" + "qL3r7sT8") → "ghi789"Соль не секретна — она хранится вместе с хэшем. Её задача — сделать rainbow table атаки бесполезными.
bcrypt — самый популярный алгоритм хэширования паролей. Специально спроектирован медленным.
Установка
Заголовок раздела «Установка»npm install bcryptjs# или нативная версия (быстрее):npm install bcryptРегистрация пользователя
Заголовок раздела «Регистрация пользователя»import bcrypt from 'bcryptjs';
async function registerUser(email, password) { // cost factor (rounds) — чем выше, тем медленнее и безопаснее // рекомендуется 10-12 для production const saltRounds = 12;
// bcrypt сам генерирует соль и хэширует const hashedPassword = await bcrypt.hash(password, saltRounds);
// Сохраняем хэш, НЕ оригинальный пароль await db.users.create({ email, password: hashedPassword });
console.log('User created!');}Вход пользователя
Заголовок раздела «Вход пользователя»async function loginUser(email, password) { const user = await db.users.findOne({ email });
if (!user) { // Важно: возвращать одинаковую ошибку для // "пользователь не найден" и "неверный пароль" // чтобы нельзя было перечислять пользователей throw new Error('Invalid credentials'); }
// bcrypt.compare безопасно сравнивает пароль с хэшем const isValid = await bcrypt.compare(password, user.password);
if (!isValid) { throw new Error('Invalid credentials'); }
return user;}Структура bcrypt хэша
Заголовок раздела «Структура bcrypt хэша»$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy│ │ │ ││ │ │ └── хэш (31 символ)│ │ └────────────────────── соль (22 символа)│ └───────────────────────── cost factor (12)└───────────────────────────── версия алгоритма (2b)Cost factor и скорость
Заголовок раздела «Cost factor и скорость»import bcrypt from 'bcryptjs';import { performance } from 'perf_hooks';
async function benchmark() { const password = 'testpassword';
for (const rounds of [8, 10, 12, 14]) { const start = performance.now(); await bcrypt.hash(password, rounds); const time = performance.now() - start;
console.log(`rounds=${rounds}: ${time.toFixed(0)}ms`); }}
benchmark();// rounds=8: ~50ms// rounds=10: ~200ms// rounds=12: ~800ms// rounds=14: ~3000msВыбирай rounds так, чтобы хэширование занимало 100-300мс на твоём сервере.
Argon2 — современная альтернатива
Заголовок раздела «Argon2 — современная альтернатива»Argon2 победил в Password Hashing Competition (PHC) 2015 года. Лучше bcrypt по нескольким параметрам.
Три варианта:
- Argon2d — защита от GPU атак (криптовалюты)
- Argon2i — защита от side-channel атак (пароли)
- Argon2id — гибрид, рекомендуется для паролей
npm install argon2import argon2 from 'argon2';
// Хэшированиеasync function hashPassword(password) { const hash = await argon2.hash(password, { type: argon2.argon2id, memoryCost: 2 ** 16, // 64 MB памяти timeCost: 3, // 3 итерации parallelism: 1, // 1 поток });
return hash;}
// Проверкаasync function verifyPassword(hash, password) { return await argon2.verify(hash, password);}
// Использованиеconst hash = await hashPassword('mypassword');const valid = await verifyPassword(hash, 'mypassword');console.log(valid); // truePBKDF2 — встроен в Node.js
Заголовок раздела «PBKDF2 — встроен в Node.js»PBKDF2 доступен из коробки в Node.js через crypto:
import { randomBytes, pbkdf2 } from 'crypto';import { promisify } from 'util';
const pbkdf2Async = promisify(pbkdf2);
async function hashPassword(password) { const salt = randomBytes(32).toString('hex'); const iterations = 100000; // минимум 100k для production const keyLen = 64; const digest = 'sha512';
const hash = await pbkdf2Async(password, salt, iterations, keyLen, digest);
return `${salt}:${iterations}:${hash.toString('hex')}`;}
async function verifyPassword(storedHash, password) { const [salt, iterations, hash] = storedHash.split(':');
const newHash = await pbkdf2Async( password, salt, parseInt(iterations), 64, 'sha512' );
// Используем timingSafeEqual для защиты от timing attacks const { timingSafeEqual } = await import('crypto'); return timingSafeEqual( Buffer.from(hash, 'hex'), newHash );}Ошибки, которых нужно избегать
Заголовок раздела «Ошибки, которых нужно избегать»MD5 и SHA-1 — НЕ для паролей!
Заголовок раздела «MD5 и SHA-1 — НЕ для паролей!»// НИКОГДА так не делай!const hash = crypto.createHash('md5').update(password).digest('hex');// MD5 слишком быстрый — 10 миллиардов хэшей в секунду на GPU!Timing attacks
Заголовок раздела «Timing attacks»// Опасно: можно измерить время сравнения и угадать хэшif (userHash === storedHash) { ... }
// Безопасно: constant-time comparisonconst { timingSafeEqual } = require('crypto');if (timingSafeEqual(Buffer.from(userHash), Buffer.from(storedHash))) { ... }
// В bcrypt это уже встроено в bcrypt.compare()Логирование паролей
Заголовок раздела «Логирование паролей»// Опасно!console.log(`User ${email} logged in with password: ${password}`);
// Никогда не логируй пароли, токены и другие секреты!console.log(`User ${email} logged in successfully`);Смена алгоритма хэширования
Заголовок раздела «Смена алгоритма хэширования»Как обновить хэши без сброса паролей:
async function loginWithRehash(email, password) { const user = await db.users.findOne({ email });
if (!user) throw new Error('Invalid credentials');
let isValid = false;
// Пробуем старый алгоритм (PBKDF2) if (user.hashVersion === 'v1') { isValid = await verifyPbkdf2(user.password, password);
// При успехе — перехэшируем в bcrypt if (isValid) { const newHash = await bcrypt.hash(password, 12); await db.users.update(user.id, { password: newHash, hashVersion: 'v2' }); } } else { isValid = await bcrypt.compare(password, user.password); }
if (!isValid) throw new Error('Invalid credentials'); return user;}Практические задания
Заголовок раздела «Практические задания»- Создай функцию регистрации и входа с bcrypt (cost factor 12)
- Сравни время выполнения bcrypt rounds 8, 10, 12, 14
- Реализуй механизм перехэширования при входе
- Добавь ограничение попыток входа (rate limiting)
| Алгоритм | Использование | Статус |
|---|---|---|
| MD5/SHA1 | Никогда | Устарел, небезопасен |
| SHA-256 | Только для проверки целостности | Не для паролей |
| PBKDF2 | Legacy, Node.js built-in | Приемлем |
| bcrypt | Пароли | Рекомендуется |
| Argon2id | Пароли | Лучший выбор |