В bcrypt.js каждый хеш включает в себя параметр cost factor (work factor), который определяет количество итераций алгоритма и напрямую влияет на вычислительную сложность вычисления хеша. Формат строки bcrypt выглядит следующим образом:
$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
Разбор структуры:
$2b$ — версия алгоритма10 — cost factor (2^10 итераций)Cost factor является частью хеша, что позволяет системе хранить информацию о том, с какой вычислительной “стоимостью” был создан пароль.
Со временем вычислительная мощность систем растёт, и значение cost factor, которое раньше считалось безопасным, перестаёт соответствовать актуальным требованиям.
Типичная ситуация:
8 или
911–14 (в зависимости от
инфраструктуры)Это приводит к неоднородности:
bcrypt.js позволяет извлечь cost factor прямо из строки хеша:
function getCostFactor(hash) {
return parseInt(hash.split('$')[2], 10);
}
const hash = "$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy";
console.log(getCostFactor(hash)); // 10
Это ключевой механизм для принятия решения о необходимости обновления хеша.
Наиболее безопасный и распространённый подход — ленивое обновление (lazy rehashing). Суть: хеш обновляется только в момент, когда пользователь успешно аутентифицируется.
Проверка и миграция в одном потоке:
import bcrypt from "bcryptjs";
const TARGET_COST = 12;
async function verifyAndUpgrade(password, user) {
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return false;
}
const currentCost = parseInt(user.passwordHash.split("$")[2], 10);
if (currentCost < TARGET_COST) {
const newHash = await bcrypt.hash(password, TARGET_COST);
await updateUserPassword(user.id, newHash);
}
return true;
}
Преимущества подхода:
Если требуется строгая унификация безопасности, используется принудительное обновление:
function needsRehash(hash, targetCost) {
const currentCost = parseInt(hash.split("$")[2], 10);
return currentCost !== targetCost;
}
При обнаружении устаревшего cost factor можно:
Для крупных систем используется пакетная обработка:
async function migrateBatch(users) {
for (const user of users) {
const cost = parseInt(user.passwordHash.split("$")[2], 10);
if (cost < 12) {
const newHash = await bcrypt.hash(user.plainPasswordBackup, 12);
await updateUserPassword(user.id, newHash);
}
}
}
Однако хранение plaintext паролей недопустимо, поэтому реальный сценарий требует:
bcrypt.js не предоставляет автоматического механизма обновления cost factor. Все решения реализуются на уровне приложения.
Ключевые ограничения:
hashLazy rehash
Forced reset
Batch migration
Практическая реализация обычно объединяет проверку и обновление:
async function loginUser(email, password) {
const user = await findUserByEmail(email);
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return null;
const currentCost = parseInt(user.passwordHash.split("$")[2], 10);
if (currentCost < 12) {
const upgradedHash = await bcrypt.hash(password, 12);
await updateUserPassword(user.id, upgradedHash);
}
return user;
}
Такой подход позволяет постепенно привести всю базу к актуальному уровню безопасности без отдельной миграции данных.
Увеличение cost factor должно учитывать баланс:
Практически применяемые значения:
На практике часто встречаются следующие проблемы:
Корректная модель всегда опирается на то, что bcrypt хеш является самодостаточным источником метаданных, включая cost factor.
Разные версии bcrypt используют одинаковый принцип хранения cost factor внутри хеша:
$2a$ — старый стандарт$2b$ — актуальный и наиболее распространённый$2y$ — специфические реализацииbcrypt.js корректно работает с этими форматами при сравнении, но при миграции важно сохранять единый формат нового хеша.