Обзор популярных алгоритмов: MD5, SHA, bcrypt, Argon2, scrypt

Хеширование паролей — процесс преобразования исходного пароля в необратимую строку фиксированной длины. В отличие от шифрования, хеш нельзя «расшифровать» обратно. Проверка пароля выполняется сравнением хеша введённого значения с сохранённым хешем в базе данных.

Основные требования к алгоритму хранения паролей:

  • необратимость;
  • устойчивость к подбору;
  • высокая вычислительная стоимость;
  • использование соли (salt);
  • защита от атак с использованием GPU и ASIC.

Для хранения пользовательских паролей нельзя использовать обычные криптографические хеш-функции вроде MD5 или SHA-256 без дополнительных механизмов защиты. Для этой задачи применяются специальные алгоритмы: bcrypt, scrypt и Argon2.


MD5

Общая характеристика

MD5 (Message Digest Algorithm 5) — криптографическая хеш-функция, разработанная Рональдом Ривестом в 1991 году. Алгоритм создаёт 128-битный хеш.

Пример MD5-хеша:

5f4dcc3b5aa765d61d8327deb882cf99

MD5 долгое время использовался для:

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

Сегодня MD5 считается криптографически небезопасным.


Недостатки MD5

Высокая скорость вычисления

Современные видеокарты способны вычислять миллиарды MD5-хешей в секунду. Это делает brute-force и dictionary-атаки чрезвычайно эффективными.

Уязвимость к коллизиям

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

MD5 имеет практические коллизии, поэтому алгоритм нельзя использовать в системах безопасности.

Отсутствие встроенной соли

MD5 не использует salt, поэтому одинаковые пароли дают одинаковые хеши.


MD5 в JavaScript

Использование библиотеки crypto-js

Установка:

npm install crypto-js

Пример:

const CryptoJS = require("crypto-js");

const password = "qwerty123";
const hash = CryptoJS.MD5(password).toString();

console.log(hash);

Результат:

dc647eb65e6711e155375218212b3964

Почему MD5 нельзя использовать для паролей

Даже при добавлении соли MD5 остаётся слишком быстрым. Скорость — преимущество для проверки файлов, но серьёзный недостаток для хранения паролей.

Современные рекомендации OWASP и NIST запрещают использовать MD5 для password hashing.


SHA

Семейство алгоритмов SHA

SHA (Secure Hash Algorithm) — семейство криптографических хеш-функций.

Наиболее известные версии:

Алгоритм Размер хеша
SHA-1 160 бит
SHA-256 256 бит
SHA-512 512 бит

SHA-1

SHA-1 долгое время считался безопасным, но сегодня также признан устаревшим.

В 2017 году Google продемонстрировал практическую коллизию SHA-1.


SHA-256

SHA-256 остаётся криптографически надёжным для проверки целостности и цифровых подписей.

Пример SHA-256:

ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f

Однако SHA-256 плохо подходит для хранения паролей по той же причине, что и MD5 — слишком высокая скорость вычисления.


Использование SHA-256 в Node.js

Встроенный модуль crypto:

const crypto = require("crypto");

const password = "qwerty123";

const hash = crypto
  .createHash("sha256")
  .update(password)
  .digest("hex");

console.log(hash);

Использование соли

Пример с salt:

const crypto = require("crypto");

const password = "qwerty123";
const salt = crypto.randomBytes(16).toString("hex");

const hash = crypto
  .createHash("sha256")
  .update(password + salt)
  .digest("hex");

console.log({
  salt,
  hash
});

Проблемы SHA для хранения паролей

Даже при использовании соли SHA-256 остаётся:

  • слишком быстрым;
  • удобным для GPU-атак;
  • неадаптивным;
  • не memory-hard.

Для password hashing нужны алгоритмы с контролируемой стоимостью вычислений.


bcrypt

Общая характеристика

bcrypt — специализированный алгоритм хеширования паролей, основанный на шифре Blowfish.

Основные преимущества:

  • встроенная соль;
  • адаптивная сложность;
  • защита от brute-force;
  • медленное вычисление.

bcrypt остаётся одним из самых популярных решений для хранения паролей.


Установка bcrypt

npm install bcrypt

Создание хеша

const bcrypt = require("bcrypt");

async function hashPassword() {
  const password = "qwerty123";

  const saltRounds = 10;

  const hash = await bcrypt.hash(password, saltRounds);

  console.log(hash);
}

hashPassword();

Пример результата:

$2b$10$9z6m5Y9f8rX4...

Проверка пароля

const bcrypt = require("bcrypt");

async function verifyPassword() {
  const password = "qwerty123";

  const hash = await bcrypt.hash(password, 10);

  const match = await bcrypt.compare(password, hash);

  console.log(match);
}

verifyPassword();

Параметр saltRounds

saltRounds определяет вычислительную стоимость.

Пример:

Значение Примерное время
8 быстро
10 стандарт
12 медленно
14+ очень медленно

Чем выше значение:

  • тем сложнее brute-force;
  • тем выше нагрузка на сервер.

Внутренняя структура bcrypt-хеша

Пример:

$2b$10$N9qo8uLOickgx2ZMRZo5e.PJQ5...

Расшифровка:

Часть Назначение
$2b$ версия алгоритма
10 cost factor
далее соль и хеш

bcrypt автоматически хранит соль внутри строки хеша.


Ограничения bcrypt

Ограничение длины пароля

bcrypt учитывает только первые 72 байта пароля.

Длинные пароли могут обрезаться.


Слабая устойчивость к современным GPU

bcrypt проектировался раньше появления мощных GPU-ферм.

Сегодня существуют более современные memory-hard алгоритмы.


scrypt

Общая характеристика

scrypt разработан Колином Персивалем в 2009 году.

Главная особенность — высокая потребность в памяти.

Это делает атаки на GPU и ASIC значительно дороже.


Преимущества scrypt

  • memory-hard архитектура;
  • встроенная соль;
  • высокая устойчивость к параллельным атакам;
  • настройка потребления RAM.

Использование scrypt в Node.js

Node.js имеет встроенную поддержку.

Генерация хеша

const crypto = require("crypto");

const password = "qwerty123";
const salt = crypto.randomBytes(16).toString("hex");

crypto.scrypt(password, salt, 64, (err, derivedKey) => {
  if (err) throw err;

  console.log(derivedKey.toString("hex"));
});

Проверка пароля

const crypto = require("crypto");

function hash(password, salt) {
  return new Promise((resolve, reject) => {
    crypto.scrypt(password, salt, 64, (err, key) => {
      if (err) reject(err);

      resolve(key.toString("hex"));
    });
  });
}

async function verify() {
  const password = "qwerty123";
  const salt = "mysalt";

  const originalHash = await hash(password, salt);

  const checkHash = await hash(password, salt);

  console.log(originalHash === checkHash);
}

verify();

Параметры scrypt

Основные параметры:

Параметр Назначение
N CPU/Memory cost
r block size
p parallelization

Пример:

crypto.scrypt(password, salt, 64, {
  N: 16384,
  r: 8,
  p: 1
}, callback);

Особенности memory-hard алгоритмов

Memory-hard алгоритмы требуют:

  • значительного объёма RAM;
  • большого числа операций чтения памяти;
  • сложной параллелизации.

Это существенно увеличивает стоимость специализированного оборудования для атак.


Argon2

Общая характеристика

Argon2 — победитель Password Hashing Competition 2015.

Считается современным стандартом password hashing.

Основные достоинства:

  • высокая устойчивость к GPU;
  • memory-hard архитектура;
  • гибкая настройка;
  • защита от side-channel атак.

Варианты Argon2

Версия Назначение
Argon2d защита от GPU
Argon2i защита от side-channel
Argon2id комбинированный вариант

На практике рекомендуется Argon2id.


Установка

npm install argon2

Создание хеша

const argon2 = require("argon2");

async function run() {
  const password = "qwerty123";

  const hash = await argon2.hash(password);

  console.log(hash);
}

run();

Проверка пароля

const argon2 = require("argon2");

async function verify() {
  const password = "qwerty123";

  const hash = await argon2.hash(password);

  const valid = await argon2.verify(hash, password);

  console.log(valid);
}

verify();

Настройка параметров

const argon2 = require("argon2");

async function run() {
  const hash = await argon2.hash("qwerty123", {
    type: argon2.argon2id,
    memoryCost: 65536,
    timeCost: 3,
    parallelism: 4
  });

  console.log(hash);
}

run();

Параметры Argon2

Параметр Назначение
memoryCost объём памяти
timeCost число итераций
parallelism степень параллелизма

Пример Argon2-хеша

$argon2id$v=19$m=65536,t=3,p=4$...

Строка содержит:

  • тип алгоритма;
  • версию;
  • параметры;
  • соль;
  • итоговый хеш.

Сравнение алгоритмов

Скорость

Алгоритм Скорость
MD5 очень высокая
SHA-256 высокая
bcrypt средняя
scrypt низкая
Argon2 низкая

Для хранения паролей предпочтительны медленные алгоритмы.


Устойчивость к GPU

Алгоритм GPU-защита
MD5 отсутствует
SHA отсутствует
bcrypt частичная
scrypt высокая
Argon2 очень высокая

Memory-hard свойства

Алгоритм Memory-hard
MD5 нет
SHA нет
bcrypt частично
scrypt да
Argon2 да

Современные рекомендации

Сценарий Рекомендуемый алгоритм
Новые проекты Argon2id
Совместимость bcrypt
Ограниченная поддержка scrypt
Проверка файлов SHA-256
Хранение паролей не MD5 и не SHA

Соль (Salt)

Назначение соли

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

Пример:

password + salt

Преимущества соли

Соль защищает от:

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

Генерация соли

const crypto = require("crypto");

const salt = crypto.randomBytes(16).toString("hex");

console.log(salt);

Pepper

Отличие от соли

Pepper — секретное значение, известное только серверу.

В отличие от соли:

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

Пример

const pepper = process.env.PEPPER;

const finalPassword = password + pepper;

Безопасная проверка хешей

Проблема timing attacks

Обычное сравнение строк может раскрывать информацию о совпадении символов.

Небезопасно:

if (hash1 === hash2) {
}

Безопасное сравнение

const crypto = require("crypto");

const match = crypto.timingSafeEqual(
  Buffer.from(hash1),
  Buffer.from(hash2)
);

Миграция со старых алгоритмов

Распространённая ситуация

Старые системы часто используют:

  • MD5;
  • SHA-1;
  • SHA-256 без соли.

Стратегия миграции

Типичная схема:

  1. Пользователь вводит пароль.
  2. Система проверяет старый хеш.
  3. После успешного входа пароль повторно хешируется через Argon2 или bcrypt.
  4. Старый хеш заменяется новым.

Практические рекомендации

Для новых проектов

Предпочтительный выбор:

Argon2id

Если нужна максимальная совместимость

Используется:

bcrypt

Минимальные рекомендации

bcrypt

cost >= 10

Argon2id

memoryCost >= 64 MB
timeCost >= 3

Типичные ошибки

Использование MD5 для паролей

Критическая ошибка безопасности.


Использование SHA-256 без соли

Одинаковые пароли дают одинаковые хеши.


Слишком маленький cost factor

Снижает устойчивость к brute-force.


Самостоятельная реализация алгоритмов

Нельзя писать собственный password hashing вручную.

Следует использовать проверенные библиотеки.


Пример полноценного хранения пароля на Argon2

Регистрация

const argon2 = require("argon2");

async function register(password) {
  const hash = await argon2.hash(password, {
    type: argon2.argon2id
  });

  return hash;
}

Авторизация

const argon2 = require("argon2");

async function login(password, storedHash) {
  const valid = await argon2.verify(
    storedHash,
    password
  );

  return valid;
}

Сводная таблица

Алгоритм Безопасность Скорость Memory-hard Рекомендация
MD5 низкая очень высокая нет не использовать
SHA-256 средняя высокая нет не для паролей
bcrypt высокая средняя частично допустимо
scrypt высокая низкая да хороший выбор
Argon2id очень высокая низкая да лучший выбор