Разработка собственного формата сериализации

Сериализация данных в криптографических библиотеках определяет способ преобразования структурированных объектов в последовательности байтов, пригодные для шифрования, передачи и последующего восстановления. В CryptoJS этот процесс тесно связан с типом WordArray, кодировками и механизмом Formatter, который управляет тем, как зашифрованные данные представляются в виде строки и обратно преобразуются в структуру, пригодную для дешифрования.


Внутренним представлением бинарных данных в библиотеке выступает CryptoJS.lib.WordArray. Этот формат хранит данные в виде массива 32-битных слов, что позволяет эффективно выполнять криптографические операции.

При сериализации происходит переход между:

  • WordArray ↔︎ строковые представления (Base64, Hex, Latin1, Utf8)
  • объектами шифрования (CipherParams) ↔︎ строками

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


Стандартные кодировки и их ограничения

CryptoJS предоставляет несколько базовых кодировок:

  • CryptoJS.enc.Hex
  • CryptoJS.enc.Base64
  • CryptoJS.enc.Utf8
  • CryptoJS.enc.Latin1

Каждая из них реализует два метода:

  • stringify(wordArray)
  • parse(string)

Пример преобразования:

const wordArray = CryptoJS.enc.Utf8.parse("test data");
const base64 = CryptoJS.enc.Base64.stringify(wordArray);
const restored = CryptoJS.enc.Base64.parse(base64);

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


Структура CipherParams и необходимость форматирования

При шифровании данные оборачиваются в объект CipherParams, содержащий:

  • ciphertext — зашифрованные данные (WordArray)
  • key — ключ (опционально)
  • iv — вектор инициализации
  • salt — соль
  • algorithm — алгоритм шифрования
  • mode и padding — параметры режима

Прямое преобразование этого объекта в строку невозможно без форматера, поскольку требуется определить структуру итогового представления.


Механизм Formatter в CryptoJS

Formatter определяет два ключевых метода:

  • stringify(cipherParams) — преобразование в строку
  • parse(str) — восстановление объекта CipherParams

Стандартный форматер OpenSSL реализует схему:

Salted__ + salt + ciphertext (Base64)

Пример реализации:

const OpenSSLFormatter = {
  stringify(cipherParams) {
    const wordArray = cipherParams.ciphertext;
    const salt = cipherParams.salt;

    if (salt) {
      const salted = CryptoJS.lib.WordArray.create(
        [0x53616c74, 0x65645f5f] // "Salted__"
      ).concat(salt).concat(wordArray);

      return CryptoJS.enc.Base64.stringify(salted);
    }

    return CryptoJS.enc.Base64.stringify(wordArray);
  },

  parse(str) {
    const words = CryptoJS.enc.Base64.parse(str);

    const sig = CryptoJS.lib.WordArray.create(words.words.slice(0, 2));
    const salted = CryptoJS.enc.Utf8.stringify(sig);

    let cipherParams = CryptoJS.lib.CipherParams.create({
      ciphertext: words
    });

    if (salted.startsWith("Salted__")) {
      cipherParams = CryptoJS.lib.CipherParams.create({
        ciphertext: CryptoJS.lib.WordArray.create(words.words.slice(2)),
        salt: CryptoJS.lib.WordArray.create(words.words.slice(2, 4))
      });
    }

    return cipherParams;
  }
};

Разработка собственного формата сериализации

Создание собственного формата требуется при необходимости:

  • оптимизации размера данных
  • внедрения версии протокола
  • добавления метаданных (userId, timestamp, nonce)
  • отказа от Base64
  • интеграции с бинарными протоколами (WebSocket, TCP)

Проектирование структуры формата

Минимально расширяемый формат должен включать:

  • сигнатуру формата (magic bytes)
  • версию
  • IV
  • salt (если используется KDF)
  • ciphertext
  • HMAC (для целостности)

Пример бинарной структуры:

[MAGIC][VERSION][IV_LEN][IV][SALT_LEN][SALT][DATA_LEN][DATA][HMAC]

Реализация сериализации в бинарный формат

Работа с WordArray позволяет собирать собственные структуры без промежуточного JSON.

Сборка структуры

const CustomFormatter = {
  stringify(cipherParams) {
    const iv = cipherParams.iv;
    const salt = cipherParams.salt;
    const ciphertext = cipherParams.ciphertext;

    const magic = CryptoJS.enc.Utf8.parse("CSTM");
    const version = CryptoJS.enc.Utf8.parse("\x01");

    const ivLen = CryptoJS.enc.Utf8.parse(String.fromCharCode(iv.sigBytes));
    const saltLen = salt ? CryptoJS.enc.Utf8.parse(String.fromCharCode(salt.sigBytes)) : CryptoJS.enc.Utf8.parse("\x00");

    const header = magic
      .concat(version)
      .concat(iv)
      .concat(salt || CryptoJS.lib.WordArray.create());

    const payload = header.concat(ciphertext);

    const base64 = CryptoJS.enc.Base64.stringify(payload);

    return base64;
  }
};

Десериализация собственного формата

Обратный процесс требует строгого контроля структуры:

const CustomFormatter = {
  parse(str) {
    const data = CryptoJS.enc.Base64.parse(str);

    const words = data.words;

    const magic = CryptoJS.lib.WordArray.create(words.slice(0, 1));
    const version = CryptoJS.lib.WordArray.create(words.slice(1, 2));

    const iv = CryptoJS.lib.WordArray.create(words.slice(2, 6));
    const ciphertext = CryptoJS.lib.WordArray.create(words.slice(6));

    return CryptoJS.lib.CipherParams.create({
      ciphertext,
      iv
    });
  }
};

Использование KDF и интеграция с сериализацией

При использовании PBKDF2 или EvpKDF соль становится обязательной частью формата.

const key = CryptoJS.PBKDF2(password, salt, {
  keySize: 256 / 32
});

Сериализация должна сохранять:

  • salt
  • iteration count (если требуется воспроизводимость)
  • hash function id

Добавление целостности через HMAC

Для защиты от модификации данных формат расширяется:

const hmac = CryptoJS.HmacSHA256(ciphertext, key);

И включается в сериализацию:

DATA || HMAC

При десериализации происходит проверка:

const recalculated = CryptoJS.HmacSHA256(ciphertext, key);

if (!CryptoJS.enc.Hex.stringify(recalculated).equals(hmac)) {
  throw new Error("Invalid integrity check");
}

Версионирование формата

Версия формата должна быть первым изменяемым параметром, так как позволяет:

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

Пример:

const FORMAT_VERSION = 2;

При парсинге:

if (version !== expectedVersion) {
  throw new Error("Unsupported format version");
}

Оптимизация размера сериализованных данных

Для уменьшения размера применяются:

  • отказ от Base64 в пользу бинарных потоков
  • сокращение заголовков
  • использование фиксированных полей вместо строковых маркеров
  • упаковка нескольких параметров в битовые флаги

Пример упаковки флагов:

bit 0 — наличие salt
bit 1 — наличие IV
bit 2 — наличие HMAC

Ошибки проектирования форматов сериализации

На практике часто возникают критические проблемы:

  • отсутствие версии формата приводит к невозможности миграции
  • использование JSON увеличивает размер и снижает производительность
  • отсутствие HMAC делает формат уязвимым к подмене
  • зависимость от UTF-8 для бинарных данных вызывает повреждение данных
  • нефиксированные поля ломают обратную совместимость

Интеграция с Cipher API CryptoJS

Собственный формат подключается через параметр format:

const encrypted = CryptoJS.AES.encrypt("data", key, {
  format: CustomFormatter
});

И обратное восстановление:

const decrypted = CryptoJS.AES.decrypt(encrypted.toString(), key, {
  format: CustomFormatter
});

Архитектурные принципы расширяемой сериализации

При проектировании формата критично учитывать:

  • неизменяемость базовой структуры
  • строгую детерминированность сериализации
  • независимость от платформы (Node.js / browser)
  • отсутствие скрытых зависимостей от кодировок окружения
  • возможность потоковой обработки данных

Применение в распределённых системах

Собственные форматы сериализации применяются при:

  • шифровании сообщений в очередях (Kafka, RabbitMQ)
  • передаче данных через WebSocket
  • хранении зашифрованных документов
  • обмене между микросервисами

В таких сценариях формат становится частью протокола, а не просто способом представления данных.