Кодек bytes: работа с типизированными массивами

Типизированные массивы в JavaScript стали основой для эффективной работы с бинарными данными, особенно в криптографических библиотеках, где важна точность представления байтов и минимальные накладные расходы на преобразования. В SJCL (Stanford Javascript Crypto Library) взаимодействие с такими данными строится вокруг внутреннего формата bitArray, а для интеграции с внешними источниками данных используется модуль sjcl.codec.bytes.

Внутри SJCL любые бинарные данные представлены не как Uint8Array, а как специализированная структура — bitArray. Это массив 32-битных слов с дополнительным элементом, описывающим количество значимых бит в последнем слове.

Такое представление удобно для криптографических операций:

  • операции выполняются на уровне слов (32 бита);
  • легко реализуются побитовые преобразования;
  • поддерживаются нестандартные длины сообщений.

Однако при взаимодействии с внешним миром — браузерными API, Node.js Buffer, сетевыми протоколами — требуется преобразование в байтовый формат.

Именно для этого используется sjcl.codec.bytes.

Кодек bytes: назначение и область применения

sjcl.codec.bytes выполняет двустороннее преобразование между:

  • массивом байтов (Array<number> с значениями 0–255);
  • внутренним форматом SJCL (bitArray).

Основные функции:

  • sjcl.codec.bytes.toBits(bytes) — преобразует байты в bitArray;
  • sjcl.codec.bytes.fromBits(bitArray) — преобразует bitArray обратно в байты.

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

Преобразование Uint8Array в bitArray

Типизированные массивы Uint8Array являются стандартом работы с бинарными данными в современном JavaScript. Однако SJCL не работает с ними напрямую, поэтому требуется явное преобразование.

const bytes = new Uint8Array([104, 101, 108, 108, 111]);

const bitArray = sjcl.codec.bytes.toBits(Array.from(bytes));

Ключевой момент заключается в том, что toBits ожидает обычный массив чисел, а не Uint8Array. Поэтому используется Array.from.

Внутренне происходит упаковка байтов в 32-битные слова:

  • каждые 4 байта объединяются в одно слово;
  • порядок байтов соответствует big-endian логике внутри SJCL;
  • длина данных фиксируется в последнем элементе массива.

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

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

const bitArray = sjcl.hash.sha256.hash("data");

const bytes = sjcl.codec.bytes.fromBits(bitArray);
const uint8 = new Uint8Array(bytes);

Функция fromBits возвращает обычный массив чисел, который легко оборачивается в Uint8Array без дополнительных преобразований.

Внутренний механизм преобразования

Упаковка байтов в 32-битные слова

При вызове toBits происходит последовательная упаковка:

  • каждый байт занимает 8 бит;
  • 4 байта формируют одно 32-битное слово;
  • если байтов недостаточно до полного слова, используется выравнивание.

Пример логики:

[0x61, 0x62, 0x63, 0x64]
→ 0x61626364

Если количество байтов не кратно четырём, последний элемент дополняется нулями, а длина сохраняется отдельно.

Разворачивание bitArray в байты

При fromBits происходит обратная операция:

  • 32-битные слова разбиваются на байты;
  • учитывается длина в битах;
  • лишние байты отбрасываются.

Это критично для корректного восстановления данных после хеширования, где длина часто не кратна 8.

Особенности работы с Typed Arrays

При интеграции SJCL с современными API возникают типовые сценарии:

Web Crypto API

crypto.subtle.digest("SHA-256", uint8)

Результат Web Crypto возвращается как ArrayBuffer, который удобно преобразовать:

const hashArray = new Uint8Array(buffer);
const sjclBits = sjcl.codec.bytes.toBits(Array.from(hashArray));

Node.js Buffer

В Node.js чаще используется Buffer, который является подтипом Uint8Array:

const buffer = Buffer.from("data");

const bits = sjcl.codec.bytes.toBits(Array.from(buffer));

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

const bytes = sjcl.codec.bytes.fromBits(bits);
const buffer = Buffer.from(bytes);

Потери и особенности при преобразованиях

Работа через bytes codec кажется простой, но имеет ряд особенностей:

1. Неявное копирование данных

Каждое преобразование:

  • создаёт новый массив;
  • увеличивает нагрузку на GC;
  • может быть узким местом в высоконагруженных системах.

2. Отсутствие прямой поддержки Uint8Array

SJCL исторически ориентирован на собственный формат данных. Поэтому:

  • нет zero-copy преобразований;
  • нет встроенной работы с ArrayBuffer.

3. Выравнивание битовой длины

bitArray хранит длину в битах, что приводит к ситуациям:

  • байтовое представление может содержать “лишние” нули;
  • важно учитывать длину при сериализации.

Пример полного цикла обработки данных

const input = new TextEncoder().encode("cryptographic data");

// Uint8Array → SJCL bitArray
const bits = sjcl.codec.bytes.toBits(Array.from(input));

// криптографическая операция
const hashBits = sjcl.hash.sha256.hash(bits);

// bitArray → bytes
const outputBytes = sjcl.codec.bytes.fromBits(hashBits);

// обратно в Uint8Array
const output = new Uint8Array(outputBytes);

Практическое значение codec.bytes в архитектуре SJCL

sjcl.codec.bytes выполняет роль адаптера между двумя мирами:

  • миром JavaScript Typed Arrays;
  • внутренним представлением SJCL.

Он используется во всех ключевых сценариях:

  • шифрование (AES, GCM, CCM);
  • хеширование (SHA-256, SHA-512);
  • HMAC;
  • генерация ключей и соли.

Без этого слоя SJCL не мог бы корректно взаимодействовать с современными API браузеров и Node.js, сохраняя при этом свой внутренний криптографический формат.

Оптимизационные аспекты

При интенсивной работе с бинарными данными стоит учитывать:

  • минимизация вызовов Array.from;
  • кэширование промежуточных bitArray;
  • группировка операций до преобразования в bytes;
  • использование Buffer напрямую в Node.js при массовых операциях.

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

Работа с нестандартными длинами данных

SJCL допускает данные произвольной длины, что приводит к особенностям при кодировании:

  • байтовый массив может быть не кратен 4;
  • последний 32-битный блок содержит только часть данных;
  • длина хранится отдельно и учитывается при декодировании.

Это позволяет корректно работать с потоковыми данными и сообщениями переменной длины, но требует аккуратного обращения при интеграции с внешними форматами сериализации.