Хранение паролей в открытом виде — критическая ошибка проектирования любой системы аутентификации. Даже при использовании HTTPS пароль неизбежно оказывается в базе данных, а значит при компрометации БД злоумышленник получает доступ ко всем учетным записям.
Для защиты применяется одностороннее хеширование паролей. Вместо исходного значения сохраняется криптографический хеш, который невозможно обратить обратно в пароль за разумное время.
В экосистеме JavaScript существует множество решений для этой задачи.
Одним из высокоуровневых инструментов является библиотека
password-hash, а современным промышленным стандартом —
алгоритм Argon2.
Несмотря на схожую цель, подходы у них радикально различаются.
npm install password-hash
const passwordHash = require('password-hash');
const password = 'myStrongPassword';
const hashedPassword = passwordHash.generate(password);
console.log(hashedPassword);
Пример результата:
sha1$3$5d41402abc4b2a76b9719d911017c592
const passwordHash = require('password-hash');
const password = 'myStrongPassword';
const hashedPassword = passwordHash.generate(password);
const isMatch = passwordHash.verify(password, hashedPassword);
console.log(isMatch);
Результат:
true
Библиотека является надстройкой над стандартными криптографическими хеш-функциями:
Внутри создается:
Типичная структура:
алгоритм$итерации$соль$хеш
Пример:
sha256$1000$saltValue$hashValue
const passwordHash = require('password-hash');
const hash = passwordHash.generate('secret', {
algorithm: 'sha256'
});
console.log(hash);
const hash = passwordHash.generate('secret', {
algorithm: 'sha512'
});
Количество итераций увеличивает стоимость перебора.
const hash = passwordHash.generate('secret', {
algorithm: 'sha512',
iterations: 5000
});
Чем больше итераций:
const hash = passwordHash.generate('secret', {
saltLength: 32
});
const passwordHash = require('password-hash');
function registerUser(login, password) {
const hash = passwordHash.generate(password, {
algorithm: 'sha512',
iterations: 10000,
saltLength: 32
});
const user = {
login,
passwordHash: hash
};
return user;
}
const user = registerUser('admin', 'superPassword');
console.log(user);
const passwordHash = require('password-hash');
const dbUser = {
login: 'admin',
passwordHash: 'sha512$10000$saltsalt$hashhash'
};
function login(password) {
return passwordHash.verify(
password,
dbUser.passwordHash
);
}
Несмотря на простоту, библиотека считается устаревшей для современных систем безопасности.
Главная причина — использование обычных криптографических хешей вместо специализированных password hashing алгоритмов.
SHA256 и SHA512 создавались не для хранения паролей.
Их задача:
Они чрезвычайно быстрые.
Высокая скорость опасна при хранении паролей.
Современные GPU способны вычислять миллиарды SHA256-хешей в секунду.
Это делает возможными:
Даже большое число итераций не решает проблему полностью.
Допустим, пароль хранится так:
sha256(password)
Видеокарта может проверить огромный словарь паролей почти мгновенно.
Если используется Argon2, ситуация меняется:
Argon2 — современный алгоритм хеширования паролей, победитель конкурса Password Hashing Competition (PHC).
Существует три варианта:
| Вариант | Назначение |
|---|---|
| Argon2d | Защита от GPU |
| Argon2i | Защита от side-channel атак |
| Argon2id | Комбинированный режим |
На практике почти всегда используется Argon2id.
Argon2 требует значительного объема памяти.
Это делает GPU и ASIC менее эффективными.
Можно отдельно регулировать:
Большой объем памяти резко ограничивает масштабируемость brute force.
Argon2 рекомендуется:
npm install argon2
const argon2 = require('argon2');
async function run() {
const password = 'superSecret';
const hash = await argon2.hash(password);
console.log(hash);
}
run();
Пример результата:
$argon2id$v=19$m=65536,t=3,p=4$...
const argon2 = require('argon2');
async function login(password, hash) {
return await argon2.verify(hash, password);
}
Количество используемой памяти.
const hash = await argon2.hash(password, {
memoryCost: 65536
});
Значение указывается в килобайтах.
Количество проходов вычислений.
const hash = await argon2.hash(password, {
timeCost: 4
});
Количество потоков.
const hash = await argon2.hash(password, {
parallelism: 2
});
const argon2 = require('argon2');
const hash = await argon2.hash(password, {
type: argon2.argon2id
});
Это рекомендуемый режим.
| Характеристика | password-hash | Argon2 |
|---|---|---|
| Основа | SHA/MD5 | Специализированный алгоритм |
| Защита от GPU | Слабая | Высокая |
| Memory-hard | Нет | Да |
| Современный стандарт | Нет | Да |
| Устойчивость к brute force | Ограниченная | Высокая |
| Гибкость настройки | Низкая | Высокая |
| Рекомендуется OWASP | Нет | Да |
Обычные алгоритмы ограничиваются вычислительной мощностью.
Argon2 дополнительно ограничивает:
GPU обладают огромным количеством ядер, но память на каждое ядро ограничена.
Из-за этого преимущества GPU уменьшаются.
Side-channel attack — атака через побочные каналы:
Argon2i и Argon2id уменьшают риск таких атак.
Обычные SHA-алгоритмы подобной защиты не имеют.
Полная немедленная миграция невозможна без знания исходных паролей.
Используется схема:
const passwordHash = require('password-hash');
const argon2 = require('argon2');
async function login(password, user) {
const oldValid = passwordHash.verify(
password,
user.passwordHash
);
if (!oldValid) {
return false;
}
const newHash = await argon2.hash(password, {
type: argon2.argon2id
});
user.passwordHash = newHash;
return true;
}
Иногда библиотека встречается:
Для новых проектов использование не рекомендуется.
Argon2 намеренно медленнее.
Это не недостаток, а часть модели безопасности.
Пример:
| Алгоритм | Среднее время |
|---|---|
| SHA256 | микросекунды |
| bcrypt | миллисекунды |
| Argon2 | десятки миллисекунд |
Параметры зависят от:
{
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 1
}
Критически небезопасно.
md5(password)
Подбирается практически мгновенно.
Без соли одинаковые пароли дают одинаковый хеш.
Это позволяет:
Опасный подход:
sha256(password + 'mySecret')
Подобные схемы обычно содержат уязвимости.
До появления Argon2 стандартом считался bcrypt.
bcrypt по-прежнему безопасен, но имеет ограничения:
Argon2 считается более современным решением.
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3
});
const valid = await argon2.verify(
user.hash,
password
);
Даже использование Argon2 не отменяет необходимости:
Слишком слабые параметры Argon2 опасны.
Слишком сильные — перегружают сервер.
Практически обычно выбирают время вычисления:
50–300 мс
на один пароль.
OWASP рекомендует:
password-hash — это удобная оболочка над обычными
хеш-функциями.
Argon2 — специализированный алгоритм, проектировавшийся
именно для защиты паролей от современных атак:
Поэтому в современных Node.js-приложениях Argon2 является предпочтительным решением практически для всех сценариев хранения паролей.