Повторное хеширование при входе пользователя

Повторное хеширование пароля при входе пользователя представляет собой механизм обновления уже существующего хеша с учётом изменившихся параметров безопасности, прежде всего — увеличения стоимости вычисления (cost factor) в bcrypt.

Алгоритм bcrypt изначально проектировался как адаптивный: его вычислительная сложность регулируется параметром cost (также называемым rounds). Чем выше значение cost, тем больше времени требуется на вычисление хеша.

Со временем вычислительные мощности увеличиваются, и ранее выбранный уровень сложности перестаёт соответствовать актуальным требованиям безопасности. Например, хеши, созданные с cost = 10 в прошлом, сегодня могут вычисляться значительно быстрее, чем это было рассчитано при их выборе. Это снижает устойчивость к атакам перебора.

Повторное хеширование при входе пользователя позволяет постепенно обновлять базу паролей без принудительного сброса паролей и без массовой миграции данных.

Как bcrypt кодирует параметры хеша

Хеш bcrypt содержит встроенную информацию о параметрах его создания. Типичный формат:

$2b$12$..............................

Разбор структуры:

  • $2b$ — версия алгоритма
  • 12$ — cost factor (в данном случае 12)
  • далее — соль и сам хеш

Таким образом, хеш самодостаточен: по нему можно определить, с какой сложностью он был создан.

Определение необходимости повторного хеширования

Повторное хеширование требуется, если:

  • текущий cost в системе увеличился
  • существующий хеш был создан с меньшим cost
  • изменились требования безопасности

В bcrypt.js можно извлечь cost из хеша:

import bcrypt from 'bcryptjs';

const hash = '$2b$10$EIXh...';

const currentRounds = bcrypt.getRounds(hash);
console.log(currentRounds); // 10

Сравнение с актуальной конфигурацией системы:

const TARGET_ROUNDS = 12;

function needsRehash(hash) {
  const currentRounds = bcrypt.getRounds(hash);
  return currentRounds < TARGET_ROUNDS;
}

Реализация повторного хеширования при входе

Процесс аутентификации включает два этапа:

  1. Проверка пароля
  2. Проверка необходимости обновления хеша

Базовая проверка пароля

const isPasswordValid = await bcrypt.compare(password, user.passwordHash);

if (!isPasswordValid) {
  throw new Error('Invalid credentials');
}

Проверка и обновление хеша

После успешной аутентификации выполняется проверка актуальности хеша:

const TARGET_ROUNDS = 12;

const currentRounds = bcrypt.getRounds(user.passwordHash);

if (currentRounds < TARGET_ROUNDS) {
  const newHash = await bcrypt.hash(password, TARGET_ROUNDS);

  await updateUserPasswordHash(user.id, newHash);
}

Особенности выполнения повторного хеширования

Повторное хеширование выполняется только после успешной проверки пароля. Это важно, поскольку:

  • исключается возможность подбора нового хеша без знания пароля
  • предотвращается утечка информации через поведение системы
  • сохраняется логика единого источника истины (пароль подтверждён → обновление допустимо)

Обработка логина с учётом rehash-процесса

Полный поток входа пользователя:

async function login(username, password) {
  const user = await getUserByUsername(username);

  if (!user) {
    throw new Error('Invalid credentials');
  }

  const isValid = await bcrypt.compare(password, user.passwordHash);

  if (!isValid) {
    throw new Error('Invalid credentials');
  }

  const TARGET_ROUNDS = 12;

  if (bcrypt.getRounds(user.passwordHash) < TARGET_ROUNDS) {
    const newHash = await bcrypt.hash(password, TARGET_ROUNDS);
    await updateUserPasswordHash(user.id, newHash);
  }

  return createSession(user);
}

Причины выполнять обновление именно при входе

Модель “lazy migration” (ленивая миграция) используется для постепенного обновления базы:

  • не требуется отдельная миграция миллионов записей
  • обновление распределяется по времени
  • нагрузка на систему равномерна
  • пользователи обновляются по мере активности

Фактически база паролей эволюционирует естественным образом.

Вопрос производительности

Повторное хеширование увеличивает нагрузку на CPU, поскольку bcrypt — алгоритм с высокой вычислительной стоимостью.

Поэтому:

  • обновление выполняется только при необходимости
  • не происходит при каждом входе
  • может быть ограничено дополнительными условиями (например, процент пользователей за сессию)

Безопасные аспекты обновления

При реализации повторного хеширования учитываются следующие свойства:

  • сравнение пароля выполняется до любого обновления
  • новый хеш создаётся только на основе уже проверенного пароля
  • невозможно извлечь пароль из хеша (односторонняя функция bcrypt)
  • соль генерируется заново при каждом хешировании

Типичные ошибки реализации

Некорректные подходы включают:

  • обновление хеша без проверки пароля
  • использование старого хеша как входного значения для генерации нового
  • попытка сравнения cost без анализа структуры хеша
  • выполнение повторного хеширования при каждой аутентификации

Управление изменением cost factor

Изменение параметра сложности происходит централизованно:

const BCRYPT_COST = parseInt(process.env.BCRYPT_COST || '12', 10);

После увеличения значения система автоматически начинает обновлять хеши пользователей при входе:

  • старые записи имеют меньший cost
  • новые создаются с увеличенным cost
  • база постепенно мигрирует без дополнительных процедур

Поведение при частичном обновлении базы

На практике одновременно могут существовать хеши с разными cost:

  • 10 — старые пользователи
  • 12 — обновлённые пользователи
  • 14 — новые регистрации

Логика повторного хеширования обеспечивает постепенное выравнивание уровня безопасности без резких изменений в инфраструктуре.