UTF-8: правильная работа с Unicode

В CryptoJS любые криптографические операции опираются не на строки JavaScript напрямую, а на внутренний формат представления данных — WordArray. Понимание того, как UTF-8 преобразуется в этот формат и обратно, является ключевым для корректной работы с любыми текстовыми данными, содержащими Unicode.


Строки JavaScript и проблема Unicode

Строки в JavaScript представлены в формате UTF-16, где:

  • символы базовой многоязычной плоскости (BMP) занимают 2 байта
  • дополнительные символы (например, редкие иероглифы, эмодзи) кодируются суррогатными парами (4 байта)

Это приводит к типичным проблемам:

  • длина строки (length) не соответствует числу символов
  • побайтовое сравнение становится некорректным
  • криптографические функции начинают работать с неверными данными при прямом использовании строк

CryptoJS решает эту проблему через явное кодирование в UTF-8.


UTF-8 как базовый слой сериализации

UTF-8 — переменно-длинная кодировка Unicode:

  • 1 байт — ASCII (0–127)
  • 2–3 байта — большинство символов европейских и мировых алфавитов
  • 4 байта — дополнительные символы (эмодзи, редкие письменности)

CryptoJS всегда преобразует строку в UTF-8 перед криптографическими операциями:

const CryptoJS = require("crypto-js");

const data = "Привет, мир!";
const utf8Data = CryptoJS.enc.Utf8.parse(data);

Здесь происходит ключевой процесс:

JavaScript string → UTF-8 bytes → WordArray


WordArray: внутренняя модель данных CryptoJS

WordArray — это массив 32-битных слов, где:

  • каждое слово содержит 4 байта
  • данные хранятся в big-endian формате
  • дополнительно хранится длина в байтах

Пример структуры:

{
  words: [0x6d795061, 0x7373776f, ...],
  sigBytes: 12
}

UTF-8 байты упаковываются в эти 32-битные блоки.


Преобразование UTF-8 строки в WordArray

Основной механизм:

const text = "CryptoJS UTF-8";
const wordArray = CryptoJS.enc.Utf8.parse(text);

Процесс внутри:

  1. строка разбивается на Unicode символы
  2. каждый символ кодируется в UTF-8 байты
  3. байты группируются в 32-битные слова

Важно учитывать:

  • символы вне ASCII увеличивают размер входных данных
  • один символ может занимать до 4 байт

Обратное преобразование: WordArray → UTF-8

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

const original = CryptoJS.enc.Utf8.stringify(wordArray);

Механизм:

  1. WordArray разбивается на байты
  2. байты интерпретируются как UTF-8 последовательность
  3. результат собирается в JavaScript строку UTF-16

Типичная ошибка: работа со строками без UTF-8

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

CryptoJS.MD5("Привет");

Правильный:

CryptoJS.MD5(CryptoJS.enc.Utf8.parse("Привет"));

Если не использовать UTF-8 явно, возможны:

  • разные хэши для одинаковых визуально строк
  • несовместимость между платформами
  • ошибки при передаче данных в API

UTF-8 и криптографические алгоритмы

Все алгоритмы CryptoJS (AES, SHA, HMAC) работают с байтами:

  • входные данные всегда должны быть нормализованы в UTF-8
  • результат — бинарный WordArray

Пример AES:

const message = "Секретное сообщение";

const encrypted = CryptoJS.AES.encrypt(
  CryptoJS.enc.Utf8.parse(message),
  "ключ"
);

Дешифрование:

const decrypted = CryptoJS.AES.decrypt(encrypted, "ключ");
const text = CryptoJS.enc.Utf8.stringify(decrypted);

Проблема суррогатных пар

JavaScript может кодировать символы вне BMP как две 16-битные единицы.

Пример:

const emoji = "?";

Фактически это:

  • U+1F600 → суррогатная пара
  • в UTF-8 → 4 байта

CryptoJS корректно обрабатывает это через UTF-8, но только при использовании Utf8.parse.

Ошибка возникает при попытке обработки как “сырых” UTF-16 данных.


Нормализация Unicode

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

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

Пример:

  • “é” → один символ
  • “e + ́” → два символа

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

Рекомендуется внешняя нормализация:

const normalized = text.normalize("NFC");
const wordArray = CryptoJS.enc.Utf8.parse(normalized);

UTF-8 и Base64 взаимодействие

Часто UTF-8 используется как промежуточный слой перед Base64:

const wordArray = CryptoJS.enc.Utf8.parse("данные");
const base64 = CryptoJS.enc.Base64.stringify(wordArray);

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

const parsed = CryptoJS.enc.Base64.parse(base64);
const text = CryptoJS.enc.Utf8.stringify(parsed);

Важно понимать:

  • Base64 не заменяет UTF-8
  • Base64 работает поверх байтового представления

Побайтовая совместимость и внешние системы

При взаимодействии с:

  • REST API
  • WebSocket
  • Node.js Buffer
  • браузерными Fetch-запросами

UTF-8 является стандартом де-факто.

Пример интеграции с Buffer:

const buffer = Buffer.from("текст", "utf8");
const wordArray = CryptoJS.lib.WordArray.create(buffer);

Частые ошибки при работе с UTF-8 в CryptoJS

1. Прямая передача строки

CryptoJS.SHA256("текст");

Риск: несовпадение хэшей


2. Игнорирование кодировки при дешифровании

CryptoJS.enc.Utf8.stringify(decrypted);

Если данные не UTF-8 → мусор в выводе


3. Смешивание кодировок

  • UTF-16 (JS строки)
  • UTF-8 (CryptoJS)
  • Base64 (транспорт)

Без явного контроля возникает потеря данных


Практический шаблон безопасной работы с UTF-8

function encodeUtf8(text) {
  return CryptoJS.enc.Utf8.parse(text);
}

function decodeUtf8(wordArray) {
  return CryptoJS.enc.Utf8.stringify(wordArray);
}

function sha256(text) {
  return CryptoJS.SHA256(encodeUtf8(text)).toString();
}

Поведение при некорректных байтах

Если данные не являются валидным UTF-8:

  • возможны символы � (replacement character)
  • строка становится частично повреждённой
  • восстановление невозможно без исходных байтов

Это особенно критично при:

  • ручной сборке WordArray
  • работе с бинарными протоколами

Итоговая модель обработки текста

Внутренний pipeline CryptoJS:

JavaScript String (UTF-16)
        ↓
Utf8.parse()
        ↓
UTF-8 bytes
        ↓
WordArray (32-bit words)
        ↓
Криптографический алгоритм
        ↓
WordArray (результат)
        ↓
Utf8.stringify() / Base64 / Hex
        ↓
JavaScript String

Эта цепочка должна соблюдаться строго для любых текстовых данных, содержащих Unicode.