При работе с библиотеками хеширования паролей в 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
Такая строка обычно содержит:
Во время десериализации библиотека разбирает строку по частям. Любое нарушение структуры приводит к ошибке.
Самая распространённая причина — неправильный тип поля в БД.
Ошибка:
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();Некоторые драйверы БД или старые системы могут сохранять хеш не в UTF-8.
Проблемный пример:
Buffer.from(hash).toString('ascii')
ASCII способен повредить часть символов.
Правильный вариант:
Buffer.from(hash).toString('utf8')
Если хеш был испорчен кодировкой, восстановить его уже невозможно.
Некоторые библиотеки возвращают 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 библиотек меняют формат сериализации.
Пример проблемы:
Хеш:
$argon2i$v=16$...
может не поддерживаться новой реализацией.
Решения:
Иногда хеш случайно копируется из логов.
Проблема:
$argon2id$v=19$m=65536...
может быть сокращён логгером:
$argon2id$v=19$m=655...
После ручного восстановления в БД данные становятся некорректными.
Хеши нельзя редактировать вручную.
Некоторые разработчики пытаются самостоятельно декодировать части хеша.
Ошибка:
const decoded = atob(hash)
Большинство password-hash форматов уже содержат внутреннюю структуру. Их нельзя декодировать как обычную Base64-строку.
Неправильное декодирование ломает salt и digest.
Классическая ошибка:
const hash = dbValue.trim()
Если библиотека использует формат, где пробелы допустимы, либо строка
содержит специальные символы в конце, trim() изменяет
содержимое.
Особенно опасно:
.replace(/\s/g, '')
Это может удалить символы внутри hash payload.
Если в БД значение отсутствует:
password = NULL
код:
await verify(password, user.password)
может привести к ошибке десериализации.
Требуется предварительная проверка:
if (!user.password) {
throw new Error('Password hash missing')
}
Хорошая практика — предварительная валидация строки.
Пример:
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() всегда возвращает
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
Это принципиально разные ситуации.
Неверный пароль — нормальное поведение.
Ошибка десериализации — проблема данных или инфраструктуры.
Если формат распознан, но считается устаревшим:
if (needsRehash(hash)) {
const newHash = await hashPassword(password)
await saveHash(user.id, newHash)
}
Такой подход позволяет:
Полезно логировать:
Пример:
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 автоматически:
Особенно осторожно следует работать с:
После записи в БД полезно выполнять контрольное чтение:
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
)
Дополнительно можно хранить:
Полезно проверять систему на некорректных данных.
Пример:
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
}
}
Подобная обёртка: