Почему MD5 и SHA не подходят для паролей

Алгоритмы 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-хэшами попадает к атакующему, начинается перебор паролей.

Схема атаки:

  1. злоумышленник берёт список популярных паролей;
  2. вычисляет SHA-256 для каждого;
  3. сравнивает результаты с базой;
  4. при совпадении получает пароль.

Пример:

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 работает чрезвычайно быстро, перебор занимает минимальное время.


Проблема GPU и специализированного оборудования

SHA-256 отлично параллелится.

Это означает, что:

  • видеокарты могут вычислять миллионы хэшей одновременно;
  • ASIC-устройства выполняют перебор с колоссальной скоростью;
  • облачные сервисы позволяют масштабировать атаки.

Для злоумышленника скорость 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'));

Оба значения будут идентичны.

Это приводит к серьёзным последствиям:

  • можно определить пользователей с одинаковыми паролями;
  • популярные пароли выявляются мгновенно;
  • массовый взлом упрощается.

Соль (Salt)

Для борьбы с одинаковыми хэшами используется соль — случайная строка, добавляемая к паролю перед хэшированием.

Пример:

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 });

Теперь одинаковые пароли создают разные хэши.


Почему соль не спасает SHA полностью

Salt решает только часть проблемы:

  • предотвращает rainbow tables;
  • скрывает одинаковые пароли;
  • усложняет массовые атаки.

Но скорость SHA остаётся прежней.

Даже с солью атакующий способен выполнять огромный объём перебора:

SHA-256 + salt = всё ещё слишком быстро

Поэтому современные системы используют специальные password hashing algorithms.


Отличие криптографического хэша от password hash

Обычный криптографический хэш

Назначение:

  • контроль целостности;
  • цифровые подписи;
  • проверка данных.

Ключевые свойства:

  • высокая скорость;
  • детерминированность;
  • минимальная нагрузка.

Password hash

Назначение:

  • защита паролей.

Ключевые свойства:

  • намеренная медленность;
  • высокая стоимость перебора;
  • защита от GPU;
  • настройка сложности;
  • встроенная соль.

Почему медленный алгоритм лучше

Предположим:

  • SHA-256 вычисляется 5 миллиардов раз в секунду;
  • bcrypt — 100 раз в секунду.

Тогда перебор:

1 000 000 вариантов:

SHA-256

доли секунды

bcrypt

несколько часов

При миллионах пользователей разница становится критической.


Устаревание MD5

MD5 считается криптографически сломанным алгоритмом.

Основные проблемы:

  • коллизии;
  • высокая скорость;
  • существующие базы rainbow tables;
  • широкая поддержка атакующих инструментов.

Коллизия — ситуация, когда разные данные дают одинаковый хэш.

Для паролей это не главная проблема MD5. Основная опасность — возможность сверхбыстрого перебора.


Проблемы SHA-1

SHA-1 также считается небезопасным:

  • обнаружены практические коллизии;
  • алгоритм устарел;
  • не рекомендуется для новых систем.

Даже SHA-256 и SHA-512, несмотря на криптографическую устойчивость, всё равно не подходят для паролей из-за скорости работы.

Это важный момент:

Криптографическая стойкость ≠ пригодность для хранения паролей

Распространённая ошибка разработчиков

Очень часто встречается код:

const crypto = require('crypto');

function hash(password) {
  return crypto
    .createHash('sha512')
    .update(password)
    .digest('hex');
}

Разработчик считает:

  • SHA-512 современный;
  • длина хэша большая;
  • алгоритм криптографически стойкий.

Но проблема не в длине хэша.

Проблема в скорости вычислений.


Попытка «усилить» SHA через многократное хэширование

Иногда используется такой подход:

function hash(password) {
  let result = password;

  for (let i = 0; i < 100000; i++) {
    result = crypto
      .createHash('sha256')
      .update(result)
      .digest('hex');
  }

  return result;
}

Это лучше, чем одиночный SHA, но остаются проблемы:

  • отсутствие защиты от GPU;
  • ошибки реализации;
  • сложность настройки;
  • риск несовместимости;
  • отсутствие стандартизации.

Специализированные библиотеки уже решают эти задачи намного надёжнее.


Современные алгоритмы для паролей

Для хранения паролей применяются:

  • bcrypt;
  • scrypt;
  • Argon2;
  • PBKDF2.

Их особенности:

  • встроенная соль;
  • настройка сложности;
  • высокая стоимость перебора;
  • защита от аппаратных атак.

Как работает bcrypt

Пример:

const bcrypt = require('bcrypt');

async function createHash(password) {
  return await bcrypt.hash(password, 12);
}

Число 12 — фактор стоимости.

Повышение значения:

  • увеличивает безопасность;
  • замедляет вычисления;
  • усложняет перебор.

Почему Password-hash библиотеки безопаснее

Специализированные библиотеки:

  • автоматически генерируют соль;
  • безопасно сравнивают значения;
  • используют проверенные алгоритмы;
  • уменьшают вероятность ошибок разработчика.

Пример проверки:

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 библиотеки обычно используют безопасное сравнение:

  • constant-time comparison;
  • минимизация утечек по времени;
  • защита от анализа выполнения.

Рост вычислительных мощностей

Пароль, защищённый SHA-256 десять лет назад, сегодня может взламываться значительно быстрее.

Причины:

  • рост мощности GPU;
  • дешёвые облачные вычисления;
  • развитие ASIC;
  • оптимизация атакующих инструментов.

Password hashing algorithms позволяют постепенно повышать сложность:

bcrypt.hash(password, 14);

или:

argon2.hash(password, {
  timeCost: 4,
  memoryCost: 65536
});

Memory-hard алгоритмы

Современные решения вроде Argon2 используют не только CPU, но и большой объём памяти.

Это делает атаки дорогими:

  • GPU теряют эффективность;
  • ASIC становятся менее выгодными;
  • массовый перебор усложняется.

Именно поэтому Argon2 считается одним из лучших вариантов для новых систем.


PBKDF2 как компромисс

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 должен быть:

  • медленным;
  • дорогим для перебора;
  • настраиваемым;
  • устойчивым к GPU;
  • устойчивым к массовым атакам;
  • уникальным для каждого пользователя.

MD5 и SHA проектировались для совершенно других задач.

Поэтому даже SHA-512 не считается правильным решением для хранения паролей в современных приложениях.