Проверка корректности хранения хешей в базе данных

Корректное хранение bcrypt-хешей в базе данных является критически важной частью безопасности аутентификации. Ошибки на этом этапе приводят не только к невозможности входа пользователей, но и к уязвимостям, связанным с подменой или повреждением хешей, а также к непредсказуемому поведению системы авторизации.

Структура bcrypt-хеша и требования к хранению

Хеш, создаваемый bcrypt.js, представляет собой строку фиксированного формата:

$2b$12$EIXCh9z8q8cQq4w2Jw0Q1uQpQYQ1p9XcQvQk9cWZk1l9cQ2h9xG5K

Он включает:

  • идентификатор алгоритма ($2b$)
  • cost factor (например, 12)
  • соль
  • итоговый хеш

Ключевое свойство: строка bcrypt всегда имеет длину около 60 символов.

Требования к полю базы данных

Ошибки часто возникают при выборе неподходящего типа поля:

  • VARCHAR(255) — безопасный стандарт
  • CHAR(60) — допустим, но менее гибкий
  • TEXT — технически допустим, но избыточен

На практике критически важно:

  • исключить усечение строки
  • учитывать кодировку (UTF-8 без преобразований)
  • избегать автоматических преобразований ORM

Типовые причины повреждения хешей

Усечение строки при записи

Наиболее распространённая проблема возникает при ограничении длины поля меньше 60 символов. В этом случае bcrypt-хеш записывается частично:

$2b$12$EIXCh9z8q8cQq4w2Jw0Q1uQpQYQ1p9XcQvQk9cWZk1l9cQ2h9xG5

Даже один потерянный символ делает проверку невозможной.

Автоматическая нормализация ORM

Некоторые ORM могут:

  • обрезать строки при сериализации
  • изменять encoding
  • применять кастомные трансформации полей

Особенно критично при использовании:

  • Sequelize
  • TypeORM
  • Mongoose (при кастомных схемах)

Проблемы кодировки

bcrypt-хеши должны храниться как ASCII-совместимые строки. Любое вмешательство в кодировку приводит к:

  • изменению символов $
  • замене специальных символов
  • невозможности сравнения

Проверка корректности сохранённого хеша

Проверка хранения выполняется не через криптографическую валидацию, а через структурную и функциональную проверку.

Структурная проверка

Базовые критерии:

  • строка начинается с $2b$, $2a$ или $2y$
  • длина около 60 символов
  • отсутствие пробелов и управляющих символов

Пример проверки:

function isValidBcryptHash(hash) {
  return typeof hash === 'string'
    && /^\$2[aby]\$\d{2}\$/.test(hash)
    && hash.length === 60;
}

Функциональная проверка через bcrypt.compare

Единственный достоверный способ проверки — попытка сравнения:

import bcrypt from 'bcryptjs';

const isValid = await bcrypt.compare('test_password', storedHash);

Если хеш повреждён, результат:

  • либо false
  • либо исключение при некорректной структуре

Проверка целостности при записи в базу данных

Валидация перед сохранением

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

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

if (!isValidBcryptHash(hash)) {
  throw new Error('Некорректный bcrypt-хеш');
}

await db.users.insert({ password_hash: hash });

Контроль длины поля

При проектировании схемы базы данных важно учитывать:

  • bcrypt всегда фиксированной длины
  • запас на возможные расширения алгоритма

Пример SQL-схемы:

password_hash VARCHAR(255) NOT NULL

Обнаружение повреждённых хешей в существующей базе

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

for (const user of users) {
  if (!isValidBcryptHash(user.password_hash)) {
    console.log('Повреждённый хеш:', user.id);
  }
}

Дополнительно применяются тестовые сравнения:

try {
  await bcrypt.compare('dummy', user.password_hash);
} catch (e) {
  console.log('Ошибка хеша:', user.id);
}

Миграция и восстановление некорректных данных

При обнаружении повреждённых значений применяется стратегия:

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

Пример генерации нового хеша:

const newHash = await bcrypt.hash(newPassword, 12);
await db.users.update(userId, { password_hash: newHash });

Версионность bcrypt-хешей

bcrypt включает идентификатор версии (2a, 2b, 2y). В системах с долгим жизненным циклом важно учитывать:

  • старые хеши могут использовать 2a
  • современные реализации используют 2b
  • сравнение всегда происходит независимо от версии
bcrypt.compare(password, hash); // версия определяется автоматически

Контроль cost factor при хранении

Хеш хранит параметр сложности (cost), например:

$2b$12$...

При проверке корректности хранения важно учитывать:

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

Повторное хеширование при обновлении политики безопасности

Если система увеличивает cost factor, применяется стратегия rehash:

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

if (match && bcrypt.getRounds(hash) < 14) {
  const newHash = await bcrypt.hash(password, 14);
  await db.users.update(id, { password_hash: newHash });
}

Защита от частично повреждённых данных

Даже при корректной длине возможны скрытые ошибки:

  • замена символов $ при сериализации JSON
  • некорректная миграция между СУБД
  • повреждение при экспорте/импорте

Признак таких ошибок — стабильный false при корректном пароле.

Особенности хранения в различных СУБД

MySQL / MariaDB

  • предпочтительно VARCHAR(255)
  • важно отключать charset преобразования

PostgreSQL

  • TEXT или VARCHAR(255)
  • строгая типизация предотвращает усечение

MongoDB

  • строковый тип без ограничений
  • риск возникает только на уровне приложения

Контрольные точки корректности хранения

Система считается корректной при выполнении условий:

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

Поддержание этих условий обеспечивает устойчивость аутентификации и исключает скрытые ошибки хранения, которые проявляются только на этапе проверки пароля.