Модуль crypto в Node.js: хеши, соль, шифрование

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов с бейджем. Код дан по документации Node.js. Говорим прямо, а не ставим бейдж «проверено».

crypto — встроенный модуль для криптографии. Тема, где ошибка стоит дорого, а половина проблем — от путаницы двух понятий. С них и начнём.

Хеш против шифрования

Самое важное различие во всей криптографии для разработчика:

Хеш против шифрования

Хеширование

Необратимо — только вперёд

  • Из хеша нельзя восстановить данные
  • Одинаковый вход → одинаковый хеш
  • Для паролей, проверки целостности
  • SHA-256, scrypt, bcrypt

Шифрование

Обратимо — с ключом назад

  • С ключом данные расшифровываются
  • Для секретов, которые надо прочитать позже
  • Токены, файлы, сообщения
  • AES-GCM

Хеш — улица с односторонним движением: из результата исходные данные не восстановить. Поэтому им проверяют пароли (сравнивают хеши, а не сами пароли) и целостность файлов.

Шифрование обратимо: зашифровали ключом — расшифровали тем же (или парным) ключом. Им защищают данные, которые нужно прочитать позже.

Грубая ошибка — «зашифровать пароль». Пароли хешируют, а не шифруют: их не нужно восстанавливать, нужно только проверять.

Хеширование данных (не паролей)

Для контрольных сумм и целостности — быстрые хеши подходят:

import { createHash } from 'node:crypto';

const hash = createHash('sha256')
  .update('содержимое файла')
  .digest('hex');
// "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"

SHA-256 здесь на месте: проверить, не изменился ли файл, сверить контрольную сумму загрузки. Тот же приём мы видели в проверке целостности после сжатия.

Пароли: медленно и с солью

А вот для паролей SHA-256 категорически не годится — и это контринтуитивно. Проблема именно в скорости: современная видеокарта считает миллиарды SHA-256 в секунду, поэтому украденную базу быстрых хешей перебирают по словарю за часы.

Пароли хешируют нарочно медленными алгоритмами с солью:

import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';

const scryptAsync = promisify(scrypt);

// при регистрации
async function hashPassword(password) {
  const salt = randomBytes(16);                        // своя соль на пользователя
  const hash = await scryptAsync(password, salt, 64);  // МЕДЛЕННО — это хорошо
  return `${salt.toString('hex')}:${hash.toString('hex')}`;
}

// при входе
async function verify(password, stored) {
  const [saltHex, hashHex] = stored.split(':');
  const salt = Buffer.from(saltHex, 'hex');
  const hash = await scryptAsync(password, salt, 64);
  return timingSafeEqual(hash, Buffer.from(hashHex, 'hex'));
}

Три обязательные детали:

  • scrypt (встроен в Node) специально медленный — это защита, а не недостаток. Альтернативы: bcrypt, argon2 (пакеты);
  • соль (randomBytes) — своя у каждого пользователя, хранится рядом с хешем. Она делает одинаковые пароли разными в базе и обесценивает заранее посчитанные «радужные таблицы»;
  • timingSafeEqual вместо === — сравнение за постоянное время, чтобы по скорости ответа нельзя было угадывать хеш по байту.

Случайность: только crypto

import { randomBytes, randomUUID } from 'node:crypto';

randomBytes(32).toString('hex');   // случайный токен (64 hex-символа)
randomUUID();                       // "3d6f0e9c-..." — случайный UUID

Math.random() для безопасности использовать нельзя. Он быстрый и удобный, но предсказуемый: по нескольким значениям вычисляется вся последовательность. Токен сессии, код сброса пароля, ключ — только через crypto, который даёт криптографически стойкую случайность.

Правило простое: если случайное значение хоть как-то связано с безопасностью — crypto.randomBytes или crypto.randomUUID, никогда Math.random.

HMAC: подпись с секретом

Обычный хеш подтверждает, что данные не изменились. HMAC добавляет секретный ключ и подтверждает ещё и кто их отправил:

import { createHmac } from 'node:crypto';

const signature = createHmac('sha256', SECRET_KEY)
  .update(payload)
  .digest('hex');

Классическое применение — проверка вебхуков: платёжный сервис подписывает уведомление HMAC-ом с вашим общим секретом, вы пересчитываете подпись и сверяете. Совпало — сообщение настоящее и не подделано. На том же принципе работают JWT-токены.

Шифрование: AES-GCM

Когда данные нужно именно защитить и прочитать позже:

import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const key = randomBytes(32);      // 256-битный ключ
const iv = randomBytes(12);       // вектор инициализации — свой на каждое шифрование

const cipher = createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([cipher.update('секрет', 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();   // подтверждает, что шифртекст не изменён

Практический совет: не изобретайте криптографию. AES-GCM — правильный современный выбор (шифрует и проверяет целостность разом), но детали (уникальный IV на каждое сообщение, хранение ключа вне кода — в переменных окружения) критичны. Для большинства задач лучше взять проверенную библиотеку или готовый сервис, чем собирать схему вручную.


Смежные темы: Buffer — crypto работает с байтами; переменные окружения — где хранить ключи; JWT-аутентификация — HMAC на практике. Полный список — в справочнике по Node.js.

Частые вопросы

Чем хеширование отличается от шифрования?
Хеш необратим: из него нельзя восстановить исходные данные, он нужен для проверки (пароли, целостность). Шифрование обратимо: с ключом данные расшифровываются обратно. Путать их — грубая ошибка безопасности.
Можно ли хешировать пароль через SHA-256?
Нет. SHA-256 слишком быстрый — его перебирают миллиардами в секунду. Пароли хешируют медленными алгоритмами с солью: scrypt (встроен в Node), bcrypt или argon2. Скорость здесь недостаток, а не достоинство.
Зачем нужна соль?
Соль — случайные данные, добавляемые к паролю перед хешированием. Она делает одинаковые пароли разными в базе и обесценивает радужные таблицы. У каждого пользователя своя соль, она хранится рядом с хешем.
Как сгенерировать безопасный случайный токен?
crypto.randomBytes(32).toString('hex') или crypto.randomUUID(). Обычный Math.random() для токенов, паролей и ключей использовать нельзя — он предсказуем. crypto даёт криптографически стойкую случайность.
Что такое HMAC?
Хеш с секретным ключом. Он подтверждает, что данные не изменены И пришли от того, кто знает ключ. Применяется для подписи вебхуков, токенов и проверки целостности сообщений.