Хранение cost factor вне хеша: смысл и риски

В алгоритме bcrypt ключевым параметром является cost factor (также называемый log rounds или work factor). Он определяет количество итераций, через которые проходит алгоритм при вычислении хеша.

2^{cost} =

Рост значения cost увеличивает вычислительную сложность формирования хеша экспоненциально. Это означает, что увеличение даже на единицу существенно замедляет процесс хеширования.

bcrypt автоматически включает cost factor внутрь итогового хеша. Строка хеша обычно выглядит следующим образом:

$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

Где:

  • $2a$ — версия алгоритма
  • 10 — cost factor
  • далее salt и сам hash

Таким образом, bcrypt является самодостаточным: для проверки пароля не требуется отдельно хранить параметры генерации.


Идея вынесения cost factor за пределы хеша

В некоторых архитектурных решениях возникает идея хранить cost factor отдельно от строки хеша, например:

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

И при верификации использовать его вручную:

bcrypt.compare(password, hashFromDb, costFromDb)

или при генерации:

bcrypt.hash(password, costFromConfig)

На первый взгляд это может показаться удобным для централизованного управления производительностью.


Потенциальные причины такого подхода

Централизованная настройка производительности

Разные окружения могут иметь разные требования:

  • продакшн — высокий cost
  • тестирование — низкий cost
  • CI — минимальный cost для ускорения прогонов

Вынесение параметра позволяет быстро переключать режимы без пересоздания хешей.


Попытка динамического управления нагрузкой

Иногда рассматривается идея адаптации cost factor:

  • снижение при высокой нагрузке
  • повышение при снижении нагрузки
  • зависимость от мощности сервера

Миграционные стратегии

При обновлении параметров безопасности иногда предполагается хранение “версии хеша” отдельно:

  • bcrypt v1 → cost 10
  • bcrypt v2 → cost 12

Критическая особенность bcrypt: self-contained hash

Ключевой архитектурный факт:

bcrypt-хеш уже содержит cost factor внутри себя и не требует внешних данных для проверки.

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

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

Риски хранения cost factor вне хеша

1. Потеря целостности данных

Если cost хранится отдельно, возникает рассинхронизация:

  • хеш создан с cost = 12
  • в базе записано cost = 10

В результате:

  • либо проверка будет медленнее/быстрее ожидаемого
  • либо будет нарушена корректность верификации

2. Уязвимость к downgrade атаке

При внешнем хранении параметра появляется возможность подмены:

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

3. Невозможность корректной миграции без дополнительной логики

При обновлении cost factor стандартный подход bcrypt:

  • пользователь логинится
  • система проверяет хеш
  • при успехе пересоздаёт хеш с новым cost

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

  • сравнение версии
  • логика принятия решения о пересоздании

Это увеличивает сложность и вероятность ошибок.


4. Нарушение переносимости хешей

bcrypt задуман как формат:

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

При внешнем cost:

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

5. Проблемы аудита безопасности

При анализе базы данных стандартный bcrypt-хеш содержит всю необходимую информацию:

  • алгоритм
  • cost
  • salt

При внешнем хранении:

  • часть информации вынесена
  • аудит требует сопоставления нескольких источников

6. Рост поверхности атаки на инфраструктуру

Дополнительное поле cost_factor:

  • может быть изменено отдельно от хеша
  • требует защиты на уровне бизнес-логики
  • увеличивает количество точек отказа

Поведение bcrypt.js в контексте cost factor

Библиотека bcrypt.js строго следует стандарту bcrypt и:

  • автоматически извлекает cost из хеша при compare
  • не требует внешних параметров
  • поддерживает встроенную совместимость формата $2a$, $2b$, $2y$

Пример:

const bcrypt = require('bcryptjs');

const hash = bcrypt.hashSync('password', 12);

bcrypt.compareSync('password', hash); // cost извлекается автоматически

Корректный подход к обновлению cost factor

Вместо внешнего хранения используется стратегия rehash on login:

const currentCost = 12;
const legacyThreshold = 10;

function verifyAndUpgrade(password, hash) {
  const isValid = bcrypt.compareSync(password, hash);

  if (!isValid) return false;

  const extractedCost = parseInt(hash.split('$')[2], 10);

  if (extractedCost < currentCost) {
    const newHash = bcrypt.hashSync(password, currentCost);
    return { valid: true, upgrade: newHash };
  }

  return { valid: true };
}

Ключевая идея:

  • cost хранится внутри хеша
  • внешняя логика используется только для миграции

Архитектурные последствия вынесения cost factor

Если cost factor становится отдельной сущностью, система фактически перестаёт использовать bcrypt как самодостаточный формат хранения паролей и превращается в гибридную модель:

  • bcrypt — только функция хеширования
  • инфраструктура — хранитель параметров безопасности

Это приводит к:

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

Практическая устойчивость встроенного подхода

Встроенный cost factor внутри bcrypt-хеша обеспечивает:

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

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