Алгоритмы MD5, SHA-1 и SHA-2 изначально создавались не для хранения паролей, а для проверки целостности данных. Их основная задача — быстро вычислять хэш произвольного содержимого:
Например:
const crypto = require('crypto');
const hash = crypto
.createHash('sha256')
.update('hello')
.digest('hex');
console.log(hash);
Результат:
2cf24dba5fb0a30e26e83b2ac5b9e29e...
Для проверки неизменности данных такой подход подходит идеально:
Именно высокая скорость делает MD5 и SHA плохим выбором для хранения паролей.
Современные процессоры способны вычислять миллиарды SHA-256-хэшей в секунду. В случае утечки базы злоумышленник может перебрать огромное количество вариантов паролей за короткое время.
Пример хранения пароля через SHA-256:
const crypto = require('crypto');
function hashPassword(password) {
return crypto
.createHash('sha256')
.update(password)
.digest('hex');
}
const passwordHash = hashPassword('qwerty123');
console.log(passwordHash);
На первый взгляд всё выглядит безопасно:
Но возникает фундаментальная проблема.
Если база с SHA-хэшами попадает к атакующему, начинается перебор паролей.
Схема атаки:
Пример:
const crypto = require('crypto');
const stolenHash =
'ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f';
const dictionary = [
'123456',
'password',
'qwerty',
'qwerty123',
'admin'
];
for (const password of dictionary) {
const hash = crypto
.createHash('sha256')
.update(password)
.digest('hex');
if (hash === stolenHash) {
console.log('Пароль найден:', password);
}
}
Поскольку SHA работает чрезвычайно быстро, перебор занимает минимальное время.
SHA-256 отлично параллелится.
Это означает, что:
Для злоумышленника скорость SHA — преимущество.
Для хранения паролей требуется противоположное свойство: вычисление должно быть медленным и дорогим.
MD5 и SHA уязвимы перед rainbow tables — заранее подготовленными таблицами соответствий:
password123 -> ef92b778...
admin -> 8c6976e5...
qwerty -> 65e84be3...
Атакующему не нужно вычислять хэш каждый раз. Достаточно найти его в готовой таблице.
Особенно опасен MD5:
Без дополнительных механизмов одинаковые пароли всегда дают одинаковый результат.
Пример:
const crypto = require('crypto');
function sha(password) {
return crypto
.createHash('sha256')
.update(password)
.digest('hex');
}
console.log(sha('123456'));
console.log(sha('123456'));
Оба значения будут идентичны.
Это приводит к серьёзным последствиям:
Для борьбы с одинаковыми хэшами используется соль — случайная строка, добавляемая к паролю перед хэшированием.
Пример:
const crypto = require('crypto');
function hashPassword(password, salt) {
return crypto
.createHash('sha256')
.update(password + salt)
.digest('hex');
}
const salt = crypto.randomBytes(16).toString('hex');
const hash = hashPassword('qwerty123', salt);
console.log({ salt, hash });
Теперь одинаковые пароли создают разные хэши.
Salt решает только часть проблемы:
Но скорость SHA остаётся прежней.
Даже с солью атакующий способен выполнять огромный объём перебора:
SHA-256 + salt = всё ещё слишком быстро
Поэтому современные системы используют специальные password hashing algorithms.
Назначение:
Ключевые свойства:
Назначение:
Ключевые свойства:
Предположим:
Тогда перебор:
1 000 000 вариантов:
доли секунды
несколько часов
При миллионах пользователей разница становится критической.
MD5 считается криптографически сломанным алгоритмом.
Основные проблемы:
Коллизия — ситуация, когда разные данные дают одинаковый хэш.
Для паролей это не главная проблема MD5. Основная опасность — возможность сверхбыстрого перебора.
SHA-1 также считается небезопасным:
Даже SHA-256 и SHA-512, несмотря на криптографическую устойчивость, всё равно не подходят для паролей из-за скорости работы.
Это важный момент:
Криптографическая стойкость ≠ пригодность для хранения паролей
Очень часто встречается код:
const crypto = require('crypto');
function hash(password) {
return crypto
.createHash('sha512')
.update(password)
.digest('hex');
}
Разработчик считает:
Но проблема не в длине хэша.
Проблема в скорости вычислений.
Иногда используется такой подход:
function hash(password) {
let result = password;
for (let i = 0; i < 100000; i++) {
result = crypto
.createHash('sha256')
.update(result)
.digest('hex');
}
return result;
}
Это лучше, чем одиночный SHA, но остаются проблемы:
Специализированные библиотеки уже решают эти задачи намного надёжнее.
Для хранения паролей применяются:
Их особенности:
Пример:
const bcrypt = require('bcrypt');
async function createHash(password) {
return await bcrypt.hash(password, 12);
}
Число 12 — фактор стоимости.
Повышение значения:
Специализированные библиотеки:
Пример проверки:
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash('secret123', 12);
const valid = await bcrypt.compare(
'secret123',
hash
);
console.log(valid);
При ручной проверке паролей иногда используется небезопасное сравнение:
if (hash1 === hash2) {
// OK
}
Такой код может быть уязвим к timing attacks.
Password-hash библиотеки обычно используют безопасное сравнение:
Пароль, защищённый SHA-256 десять лет назад, сегодня может взламываться значительно быстрее.
Причины:
Password hashing algorithms позволяют постепенно повышать сложность:
bcrypt.hash(password, 14);
или:
argon2.hash(password, {
timeCost: 4,
memoryCost: 65536
});
Современные решения вроде Argon2 используют не только CPU, но и большой объём памяти.
Это делает атаки дорогими:
Именно поэтому Argon2 считается одним из лучших вариантов для новых систем.
PBKDF2 встроен в Node.js:
const crypto = require('crypto');
crypto.pbkdf2(
'password',
'salt',
100000,
64,
'sha512',
(err, derivedKey) => {
console.log(
derivedKey.toString('hex')
);
}
);
Особенности:
Но PBKDF2 хуже сопротивляется GPU по сравнению с Argon2 и scrypt.
Безопасный password hash должен быть:
MD5 и SHA проектировались для совершенно других задач.
Поэтому даже SHA-512 не считается правильным решением для хранения паролей в современных приложениях.