Кодировка входных данных и потенциальные ловушки

Как bcrypt.js обрабатывает входные строки

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

В JavaScript строки представлены в UTF-16, тогда как bcrypt изначально проектировался с расчётом на байтовые последовательности, чаще всего интерпретируемые через UTF-8. bcrypt.js выполняет внутреннее преобразование, но это не исключает ряда тонких проблем, связанных с кодировками и нормализацией строк.

Unicode и различие представлений одного и того же текста

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

Например:

  • символ «é» может быть представлен как один Unicode-символ U+00E9
  • либо как комбинация «e» + комбинирующий акцент U+0301

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

Это приводит к тому, что:

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

Нормализация Unicode перед хешированием

Для устранения неоднозначностей используется Unicode Normalization Form C (NFC) или Form D (NFD). В большинстве случаев рекомендуется NFC как более предсказуемая форма.

Типичная проблема возникает в сценариях:

  • пользователь регистрируется с паролем, содержащим составные символы
  • позже вводит визуально тот же пароль, но в другой нормализованной форме

bcrypt.js не выполняет нормализацию автоматически, поэтому ответственность полностью ложится на уровень приложения.

Рекомендуемый подход:

  • приводить строку к единой форме до вызова bcrypt.hash
  • использовать одинаковую нормализацию при регистрации и при проверке пароля

UTF-8 vs UTF-16: скрытые различия

JavaScript-строки используют UTF-16, но большинство серверных систем и криптографических алгоритмов ожидают UTF-8.

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

Проблемные сценарии:

  • наличие символов вне BMP (Basic Multilingual Plane), например эмодзи или редких иероглифов
  • символы, занимающие суррогатные пары в UTF-16

Такие символы могут:

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

Невидимые символы и их влияние на хеш

Одна из наиболее опасных категорий проблем — скрытые символы:

  • пробелы в начале или конце строки
  • неразрывные пробелы (U+00A0)
  • нулевые ширины символов (U+200B, U+200C, U+200D)
  • управляющие символы

bcrypt.js не выполняет очистку входных данных, поэтому каждая такая деталь становится частью хеша.

Это приводит к ситуациям, когда:

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

Проблемы сериализации входных данных

В реальных приложениях пароль может проходить через несколько слоёв:

  • HTTP-запрос
  • промежуточное API
  • JSON-сериализация
  • ORM или middleware

На каждом этапе возможны изменения строки:

  • автоматическое приведение типов
  • экранирование символов
  • удаление управляющих последовательностей
  • тримминг пробелов

bcrypt.js получает уже «готовую» строку, но не контролирует её происхождение.

Критическая ошибка архитектуры — разное преобразование пароля на этапе регистрации и на этапе проверки.

Различия между окружениями Node.js и браузера

bcrypt.js используется как в браузере, так и в Node.js, и это создаёт дополнительные расхождения:

  • различная обработка Unicode API
  • отличия в реализации Buffer и TextEncoder
  • различия в производительности преобразования строк

Особенно заметны проблемы при:

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

Опасность двойного кодирования

Распространённая ошибка — повторное преобразование строки перед передачей в bcrypt:

  • JSON.stringify(password)
  • encodeURIComponent(password)
  • Buffer.from(password).toString()

Такие операции могут:

  • изменить исходные байты строки
  • добавить escape-последовательности
  • привести к различию между «ожидаемым» и фактическим паролем

bcrypt.js не способен отличить исходный пароль от уже модифицированного представления, он работает строго с тем, что ему передано.

Проблемы сравнения при проверке пароля

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

Типичные причины ошибок:

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

Рекомендации по унификации входных данных

Для минимизации проблем на уровне кодировок используется строгий пайплайн обработки:

  • приведение строки к Unicode NFC
  • отказ от любых автоматических преобразований строки
  • исключение trim() в неожиданных местах бизнес-логики
  • контроль всех промежуточных слоёв передачи данных

Особое внимание требуется уделять:

  • формам ввода на клиенте
  • API-слоям
  • middleware, работающим с телом запроса

Потенциальные уязвимости, связанные с кодировкой

Некорректная обработка кодировок может привести не только к ошибкам, но и к уязвимостям:

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

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

Стабильность хеша при одинаковом вводе

bcrypt.js гарантирует одинаковый результат только при идентичной входной строке на уровне байтов.

Любое отклонение в:

  • нормализации
  • кодировке
  • скрытых символах
  • промежуточной обработке

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

Это фундаментальное свойство, которое делает bcrypt устойчивым к атаке перебора, но одновременно повышает требования к корректной обработке входных данных на уровне приложения.