Ошибки при десериализации хеша из базы данных

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

Типичный сценарий:

const isValid = await passwordHash.verify(password, hashFromDatabase)

Если hashFromDatabase повреждён, изменён, сохранён в неправильной кодировке или обрезан, библиотека выбрасывает исключение.

Наиболее частые симптомы:

Invalid hash format
Cannot parse hash
Unexpected end of input
Unknown algorithm
Malformed hash

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


Как устроен сериализованный хеш

Большинство современных password-hash библиотек сохраняют хеш не как набор бинарных данных, а как строку специального формата.

Пример для Argon2:

$argon2id$v=19$m=65536,t=3,p=4$YWFhYWFhYWFhYWFhYQ$K8x7D5kQ9fYx...

Пример для bcrypt:

$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36

Такая строка обычно содержит:

  • идентификатор алгоритма;
  • версию;
  • параметры вычисления;
  • salt;
  • итоговый hash.

Во время десериализации библиотека разбирает строку по частям. Любое нарушение структуры приводит к ошибке.


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

Самая распространённая причина — неправильный тип поля в БД.

Ошибка:

password VARCHAR(50)

bcrypt-хеш может занимать 60 символов, Argon2 — значительно больше.

При сохранении строка обрезается:

$argon2id$v=19$m=65536,t=3,p=4$YWFhYWFhYWFhYWFhYQ

После чтения библиотека уже не может распарсить структуру.

Правильный вариант:

password TEXT

или:

password VARCHAR(255)

Для Argon2 рекомендуется резервировать минимум 255 символов.


Потеря специальных символов

Некоторые ORM, сериализаторы или промежуточные обработчики могут изменять содержимое строки.

Опасные символы:

$
/
+
=

Пример повреждения:

$argon2id$v=19$m=65536,t=3,p=4

может превратиться в:

argon2idv=19m=65536,t=3,p=4

В результате десериализатор теряет структуру токенов.

Особенно часто это происходит:

  • при ручной очистке строк;
  • при использовании .replace();
  • при HTML-экранировании;
  • при неправильной сериализации JSON;
  • при передаче через query string.

Ошибки кодировки

Некоторые драйверы БД или старые системы могут сохранять хеш не в UTF-8.

Проблемный пример:

Buffer.from(hash).toString('ascii')

ASCII способен повредить часть символов.

Правильный вариант:

Buffer.from(hash).toString('utf8')

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


Сохранение Buffer вместо строки

Некоторые библиотеки возвращают Buffer.

Ошибка:

const hash = await createHash(password)

await users.insert({
  password: hash
})

Если ORM ожидает строку, бинарные данные могут быть сериализованы некорректно.

Безопасный вариант:

await users.insert({
  password: hash.toString()
})

или:

const hash = Buffer.from(raw).toString('base64')

Двойная сериализация

Иногда хеш случайно сериализуют несколько раз подряд.

Ошибка:

const stored = JSON.stringify(hash)

В БД попадает:

"$argon2id$v=19$..."

После чтения приложение получает строку с лишними кавычками:

"$argon2id$v=19$..."

Парсер библиотеки уже не распознаёт формат.

Исправление:

const hash = JSON.parse(stored)

или отказ от лишней сериализации.


Ошибки миграции между алгоритмами

После перехода с bcrypt на Argon2 база может содержать смешанные форматы:

$2b$10$...
$argon2id$v=19$...

Если приложение ожидает только один формат:

await argon2.verify(hash, password)

bcrypt-хеш вызовет исключение десериализации.

Правильная стратегия — определять тип алгоритма автоматически:

function detectAlgorithm(hash) {
  if (hash.startsWith('$2')) {
    return 'bcrypt'
  }

  if (hash.startsWith('$argon2')) {
    return 'argon2'
  }

  throw new Error('Unknown hash format')
}

Несовместимость версий библиотеки

Некоторые версии password-hash библиотек меняют формат сериализации.

Пример проблемы:

  • старое приложение использовало Argon2 v16;
  • новое приложение ожидает v19.

Хеш:

$argon2i$v=16$...

может не поддерживаться новой реализацией.

Решения:

  • обновлять хеши постепенно;
  • хранить версию алгоритма;
  • поддерживать legacy-режим;
  • выполнять rehash после успешного логина.

Повреждение при логировании

Иногда хеш случайно копируется из логов.

Проблема:

$argon2id$v=19$m=65536...

может быть сокращён логгером:

$argon2id$v=19$m=655...

После ручного восстановления в БД данные становятся некорректными.

Хеши нельзя редактировать вручную.


Ошибки Base64-декодирования

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

Ошибка:

const decoded = atob(hash)

Большинство password-hash форматов уже содержат внутреннюю структуру. Их нельзя декодировать как обычную Base64-строку.

Неправильное декодирование ломает salt и digest.


Использование trim()

Классическая ошибка:

const hash = dbValue.trim()

Если библиотека использует формат, где пробелы допустимы, либо строка содержит специальные символы в конце, trim() изменяет содержимое.

Особенно опасно:

.replace(/\s/g, '')

Это может удалить символы внутри hash payload.


Неправильная работа с NULL

Если в БД значение отсутствует:

password = NULL

код:

await verify(password, user.password)

может привести к ошибке десериализации.

Требуется предварительная проверка:

if (!user.password) {
  throw new Error('Password hash missing')
}

Проверка формата перед verify()

Хорошая практика — предварительная валидация строки.

Пример:

function validateHash(hash) {
  if (typeof hash !== 'string') {
    return false
  }

  if (!hash.startsWith('$')) {
    return false
  }

  return true
}

Использование:

if (!validateHash(hash)) {
  throw new Error('Invalid hash')
}

await verify(password, hash)

Обработка ошибок verify()

Нельзя предполагать, что verify() всегда возвращает false.

Многие библиотеки выбрасывают исключение:

try {
  const valid = await verify(password, hash)
} catch (err) {
  console.error(err)
}

Типичная ошибка:

const valid = await verify(password, hash)

без try/catch.

В production это может привести к падению процесса.


Отличие ошибки десериализации от неверного пароля

Неверный пароль:

false

Повреждённый hash:

Invalid hash format

Это принципиально разные ситуации.

Неверный пароль — нормальное поведение.

Ошибка десериализации — проблема данных или инфраструктуры.


Автоматическое восстановление через rehash

Если формат распознан, но считается устаревшим:

if (needsRehash(hash)) {
  const newHash = await hashPassword(password)

  await saveHash(user.id, newHash)
}

Такой подход позволяет:

  • постепенно обновлять алгоритмы;
  • избегать массовой миграции;
  • устранять старые форматы.

Диагностика проблем в production

Полезно логировать:

  • длину hash;
  • первые символы;
  • используемый алгоритм;
  • версию библиотеки;
  • тип кодировки;
  • источник данных.

Пример:

console.error({
  hashLength: hash.length,
  prefix: hash.slice(0, 15),
  version: process.version
})

Нельзя логировать полный hash в production-журналы.


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

Минимальная длина может использоваться как быстрый фильтр.

Пример:

if (hash.length < 20) {
  throw new Error('Corrupted hash')
}

Для bcrypt:

60 символов

Для Argon2:

обычно 80–120+

Слишком короткая строка почти всегда означает повреждение.


Проверка алгоритма через регулярные выражения

Пример для bcrypt:

const bcryptRegex =
  /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/

Для Argon2:

const argonRegex =
  /^\$argon2(id|i|d)\$/

Использование:

if (!argonRegex.test(hash)) {
  throw new Error('Unsupported hash format')
}

Проблемы ORM и сериализаторов

Некоторые ORM автоматически:

  • обрезают строки;
  • меняют кодировку;
  • конвертируют Buffer;
  • экранируют специальные символы.

Особенно осторожно следует работать с:

  • legacy ORM;
  • MongoDB adapters;
  • кастомными сериализаторами;
  • GraphQL middleware;
  • Redis wrappers.

После записи в БД полезно выполнять контрольное чтение:

await save(hash)

const restored = await load()

console.log(hash === restored)

Безопасная схема хранения

Рекомендуемая структура:

CRE ATE   TABLE users (
  id BIGINT PRIMARY KEY,
  password_hash TEXT NOT NULL,
  hash_algorithm VARCHAR(20),
  created_at TIMESTAMP
)

Дополнительно можно хранить:

  • версию алгоритма;
  • дату rehash;
  • параметры cost factor.

Тестирование повреждённых хешей

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

Пример:

const invalidHashes = [
  '',
  '123',
  '$argon2',
  '$2b$',
  null,
  undefined
]

Тест:

for (const hash of invalidHashes) {
  try {
    await verify(password, hash)
  } catch (err) {
    console.log('Handled')
  }
}

Такие проверки предотвращают аварии в production.


Типичная архитектура безопасной проверки

async function safeVerify(password, hash) {
  if (typeof hash !== 'string') {
    return false
  }

  if (!hash.startsWith('$')) {
    return false
  }

  try {
    return await verify(password, hash)
  } catch {
    return false
  }
}

Подобная обёртка:

  • защищает от падений;
  • фильтрует повреждённые данные;
  • централизует обработку ошибок;
  • упрощает миграцию алгоритмов.