Хеширование паролей отличается от обычного вычисления хеша данных. Для защиты пользовательских паролей недостаточно применить быстрый алгоритм вроде SHA-256. Современная система хранения паролей должна противостоять:
Библиотека Password-hash в JavaScript обычно строится вокруг трёх фундаментальных механизмов:
Эти компоненты работают совместно и формируют основу безопасного хранения паролей.
Соль — это случайная строка байтов, которая добавляется к паролю перед хешированием.
Пример:
password = "qwerty123"
salt = "A91xK2Lm"
hash(password + salt)
Даже если два пользователя имеют одинаковый пароль, итоговые хеши будут различаться.
Без соли одинаковые пароли создают одинаковые хеши.
Пример:
SHA256("123456") =
8d969eef6ecad3c29a3a629280e686cf...
Если в базе несколько одинаковых значений:
8d969eef6ecad3c29...
8d969eef6ecad3c29...
8d969eef6ecad3c29...
становится очевидно:
Радужная таблица — это заранее вычисленный набор:
пароль → хеш
Пример:
123456 → 8d969eef...
password → 5e884898...
qwerty → 65e84be3...
Если система использует чистый SHA-256 без соли, злоумышленник может мгновенно сравнить украденные хеши с готовой таблицей.
С добавлением соли:
hash("123456" + "abc")
hash("123456" + "xyz")
результаты полностью различаются.
Теперь злоумышленнику пришлось бы строить отдельную радужную таблицу для каждой соли, что делает атаку практически бессмысленной.
Для генерации соли необходим криптографически стойкий генератор случайных чисел.
Node.js:
const crypto = require('crypto');
const salt = crypto.randomBytes(16).toString('hex');
console.log(salt);
Пример результата:
f8a91cc8b7d14d2e5a71d9a3e2bc9911
Ошибка:
const salt = Math.random().toString();
Проблемы:
Для паролей допускается только:
crypto.randomBytes;Рекомендуемые размеры:
| Размер | Безопасность |
|---|---|
| 8 байт | Минимально допустимо |
| 16 байт | Стандарт |
| 32 байта | Повышенная защита |
Чаще всего используется:
crypto.randomBytes(16)
Соль не является секретом.
Допускается хранение:
Пример:
{
"user": "alex",
"salt": "f8a91cc8...",
"hash": "8b2f3c1d..."
}
При авторизации:
Пример:
const crypto = require('crypto');
function hashPassword(password, salt) {
return crypto
.createHash('sha256')
.update(password + salt)
.digest('hex');
}
Pepper — это дополнительный секретный ключ, общий для всей системы.
В отличие от соли:
| Salt | Pepper |
|---|---|
| уникален для пользователя | общий для системы |
| хранится в БД | хранится отдельно |
| не секретен | секретен |
Пример:
hash(password + salt + pepper)
Даже если база данных украдена, злоумышленник не сможет проверить пароли без знания pepper.
Правильные места хранения:
Пример:
const pepper = process.env.PEPPER_SECRET;
Неправильно:
{
"salt": "...",
"pepper": "...",
"hash": "..."
}
Если злоумышленник получает базу полностью, защита исчезает.
Типичная схема:
hash(password + userSalt + globalPepper)
Где:
userSalt — уникальна;globalPepper — секретен;Современные видеокарты способны вычислять миллиарды SHA-хешей в секунду.
Простой SHA-256 слишком быстрый:
hash(password)
Это позволяет злоумышленнику быстро перебрать огромные словари.
Парольное хеширование должно быть:
Именно поэтому используются:
PBKDF2 многократно повторяет вычисление хеша.
Упрощённо:
hash(hash(hash(hash(...))))
Тысячи или сотни тысяч раз.
const crypto = require('crypto');
crypto.pbkdf2(
'password123',
'randomSalt',
100000,
64,
'sha512',
(err, derivedKey) => {
console.log(derivedKey.toString('hex'));
}
);
Исходный пароль:
'password123'
Случайная соль:
'randomSalt'
Количество повторений:
100000
Чем больше значение:
Размер итогового ключа:
64
Алгоритм хеширования:
'sha512'
Допустим:
1 хеш = 0.000001 сек
Миллион паролей:
≈ 1 секунда
Если:
100000 итераций
тогда:
1 пароль ≈ 0.1 сек
Миллион попыток:
≈ 27 часов
На практике атака становится существенно дороже.
bcrypt использует параметр стоимости:
const bcrypt = require('bcrypt');
bcrypt.hash(password, 12);
Число 12 — это cost factor.
bcrypt увеличивает сложность по степени двойки.
| Cost | Относительная сложность |
|---|---|
| 10 | 2^10 |
| 11 | 2^11 |
| 12 | 2^12 |
| 13 | 2^13 |
Каждое увеличение на 1 почти удваивает время вычисления.
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash('password123', 12);
const match = await bcrypt.compare(
'password123',
hash
);
console.log(match);
bcrypt автоматически:
Argon2 считается одним из наиболее защищённых алгоритмов.
Он учитывает:
GPU хорошо выполняют параллельные вычисления, но память ограничена.
Argon2 делает атаку дорогой не только по CPU, но и по RAM.
const argon2 = require('argon2');
const hash = await argon2.hash('password123');
const valid = await argon2.verify(
hash,
'password123'
);
| Вариант | Назначение |
|---|---|
| Argon2d | защита от GPU |
| Argon2i | защита от side-channel |
| Argon2id | комбинированный вариант |
Обычно рекомендуется:
Argon2id
Полноценная схема:
hash(
password +
uniqueSalt +
globalPepper
)
↓
100000+ iterations
или:
Argon2id(password, salt, memoryCost)
Ошибка:
crypto.createHash('sha256')
Причина:
Плохо:
const salt = "STATIC_SALT";
Соль обязана быть уникальной.
Неправильно:
crypto.randomBytes(2)
2 байта легко перебираются.
Без pepper утечка базы опаснее.
Pepper создаёт дополнительный уровень защиты.
Плохо:
1000 iterations
Современные рекомендации значительно выше.
iterations: 100000+
digest: SHA-512
salt: 16+ bytes
cost factor: 10–14
Подбирается по производительности сервера.
Рекомендуется:
Пример:
{
"id": 15,
"email": "user@example.com",
"passwordHash": "$argon2id$v=19$m=65536,t=3,p=4$..."
}
Современные алгоритмы часто включают внутрь:
Алгоритм:
1. Пользователь вводит пароль
2. Из БД извлекается hash
3. Алгоритм извлекает salt и параметры
4. Добавляется pepper
5. Выполняется повторное хеширование
6. Результат сравнивается
Парольные хеши не предназначены для расшифровки.
Хеширование:
password → hash
является односторонней функцией.
Проверка выполняется повторным вычислением:
hash(input) === storedHash
MD5(password)
или:
SHA1(password)
Сегодня такие схемы считаются небезопасными.
Argon2id
+ unique salt
+ secret pepper
+ memory hard
+ configurable cost
Именно такая архитектура применяется в современных безопасных системах аутентификации.