Ошибки при работе с кодировками

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


Внутренняя модель данных Crypto-js

Основной тип, с которым работает библиотека, — WordArray. Он представляет собой массив 32-битных слов и используется как промежуточное представление байтов.

Ключевые моменты:

  • строка не является байтовым массивом
  • WordArray — это бинарное представление
  • кодировки используются только на этапе преобразования строк ↔︎ WordArray

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

  • Utf8
  • Latin1
  • Hex
  • Base64

Каждый из них по-разному интерпретирует входные данные, что и становится источником ошибок.


Ошибка 1: неявное использование UTF-8

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

const encrypted = CryptoJS.AES.encrypt("тест", "key");

Внутри Crypto-js происходит:

  • строка преобразуется в UTF-8
  • затем в WordArray
  • затем шифруется

Проблема возникает, если на другой стороне используется другая кодировка (например, Latin1), что приводит к некорректной расшифровке.


Ошибка 2: несоответствие UTF-8 и Latin1

Latin1 часто используется по умолчанию в старых примерах, что создаёт несовместимость.

const wordArray = CryptoJS.enc.Latin1.parse("тест");

Такой код приводит к:

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

UTF-8 является обязательным стандартом для Unicode-данных, тогда как Latin1 ограничен 256 символами.


Ошибка 3: неправильное преобразование WordArray обратно в строку

Типичная ошибка возникает при использовании .toString() без указания кодировки.

const decrypted = bytes.toString();

По умолчанию используется UTF-8, но если данные были закодированы иначе, результат становится повреждённым.

Корректный подход:

bytes.toString(CryptoJS.enc.Utf8);

или

bytes.toString(CryptoJS.enc.Hex);

в зависимости от исходного формата.


Ошибка 4: Base64 как промежуточный формат без явного контроля

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

const b64 = CryptoJS.enc.Base64.stringify(wordArray);
const parsed = CryptoJS.enc.Base64.parse(b64);

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

  • повторное кодирование Base64
  • интерпретация Base64 как UTF-8 строки
  • передача через URL без декодирования

Base64 не является строкой текста, а представляет бинарные данные в текстовом виде.


Ошибка 5: JSON и автоматическое экранирование символов

При шифровании JSON-объектов часто возникает двойное кодирование:

const data = JSON.stringify(obj);
const encrypted = CryptoJS.AES.encrypt(data, key);

Ошибка появляется при:

  • повторном JSON.stringify после расшифровки
  • неправильной интерпретации escape-символов
  • потере кодировки UTF-8 внутри JSON

Особенно критично при работе с кириллицей и emoji, которые занимают несколько байтов в UTF-8.


Ошибка 6: несовпадение кодировок при хранении IV и salt

В режимах CBC и PBKDF2 используются дополнительные бинарные параметры:

  • IV (инициализационный вектор)
  • salt (соль)

Неправильное преобразование:

const ivString = iv.toString();

Проблема:

  • теряется бинарная структура
  • строка становится неинтерпретируемой
  • восстановление шифра невозможно

Корректный вариант:

const ivBase64 = CryptoJS.enc.Base64.stringify(iv);

Ошибка 7: работа с URL и потеря символов

При передаче зашифрованных данных через URL часто применяется encodeURIComponent, но без обратного декодирования:

const url = "?data=" + encodeURIComponent(ciphertext);

Ошибки:

  • потеря символов “+”
  • искажение Base64
  • обрезка данных при копировании

Base64 должен декодироваться обратно перед расшифровкой.


Ошибка 8: различие окружений Node.js и браузера

В разных средах строки интерпретируются по-разному:

  • Node.js использует Buffer
  • браузер использует UTF-16 строки

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

Buffer.from(string)

и

CryptoJS.enc.Utf8.parse(string)

дают разные результаты при наличии не-ASCII символов.


Ошибка 9: BOM (Byte Order Mark)

UTF-8 строки могут содержать BOM, который не всегда учитывается Crypto-js.

Последствия:

  • некорректное шифрование первой части строки
  • несовпадение хэшей
  • ошибки сравнения строк после расшифровки

Ошибка 10: нормализация Unicode

Одинаково выглядящие строки могут иметь разное внутреннее представление:

  • NFC (нормализованная форма)
  • NFD (разложенная форма)

Crypto-js не выполняет автоматическую нормализацию.

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

  • “é” как один символ
  • “e + ´” как два символа

Хеши и шифрование дают разные результаты.


Типовые источники повреждения данных

Основные причины ошибок кодировок:

  • неявное использование UTF-8
  • смешивание Base64 и UTF-8
  • отсутствие явного указания enc
  • потеря бинарных данных при сериализации
  • автоматическое преобразование JSON
  • передача через URL без декодирования

Преобразования, требующие явного контроля

Все операции должны учитывать формат данных:

  • строка → WordArray (Utf8.parse)
  • WordArray → строка (Utf8.stringify)
  • бинарные данные → Base64
  • Base64 → бинарные данные
  • hex ↔︎ WordArray

Особенности работы с кириллицей и многобайтовыми символами

UTF-8 кодирует символы переменной длиной:

  • ASCII — 1 байт
  • кириллица — 2 байта
  • emoji — 4 байта

Ошибки возникают при:

  • интерпретации как Latin1
  • обрезке строки по байтам
  • неправильном разбиении WordArray

Поведение Crypto-js при отсутствии явной кодировки

При вызове:

CryptoJS.AES.encrypt(data, key)

внутренне применяется UTF-8, но при дальнейших преобразованиях .toString() может использовать Base64 по умолчанию, что создаёт скрытые несоответствия при интеграции с другими системами.


Рекомендации по устранению проблем кодировок

  • всегда явно указывать кодировку при parse и stringify
  • не использовать Latin1 для Unicode-данных
  • избегать неявного .toString() без параметров
  • контролировать Base64 как бинарный формат, а не строку
  • учитывать различия UTF-8 и UTF-16 в средах выполнения
  • нормализовать Unicode перед шифрованием
  • избегать передачи шифротекста через URL без строгого декодирования