Взаимодействие с ArrayBuffer и Uint8Array

В современных веб-приложениях криптография почти всегда работает с бинарными данными. Браузерные API, WebCrypto, файловые API и сетевые протоколы оперируют ArrayBuffer и Uint8Array, тогда как SJCL использует собственное представление — bitArray. Именно на стыке этих двух миров чаще всего возникают ошибки, потери данных и неоптимальные преобразования.

В основе SJCL лежит структура bitArray — массив 32-битных слов с дополнительным элементом длины. Это не байтовый массив в привычном смысле, а логическое представление битовой последовательности.

Ключевая особенность:

  • данные хранятся в 32-битных словах
  • длина хранится в последнем элементе массива
  • операции ориентированы на битовую арифметику

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

[0x61626300, 0x00000018] // строка "abc" в битовом представлении

Такой формат удобен для криптографических операций, но несовместим напрямую с Web API.


Uint8Array и ArrayBuffer как стандарт бинарных данных

В браузерной и Node.js среде стандартом являются:

  • ArrayBuffer — сырые бинарные данные
  • Uint8Array — представление этих данных как массива байтов

Пример:

const buffer = new ArrayBuffer(3);
const view = new Uint8Array(buffer);

view[0] = 0x61;
view[1] = 0x62;
view[2] = 0x63;

В результате получается строка "abc" в байтовом виде.

SJCL не работает напрямую с такими структурами, поэтому требуется слой преобразования.


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

Одна из наиболее частых операций — преобразование входных данных из Web API в формат SJCL.

Базовый подход:

function uint8ArrayToBitArray(u8) {
    const len = u8.length;
    const result = [];

    let i;
    for (i = 0; i < len; i += 4) {
        result.push(
            (u8[i] << 24) |
            (u8[i + 1] << 16) |
            (u8[i + 2] << 8) |
            (u8[i + 3] || 0)
        );
    }

    const bitLen = len * 8;
    result.push(bitLen);

    return result;
}

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

  • байты упаковываются в 32-битные слова big-endian
  • последний элемент хранит длину в битах
  • важно корректно обрабатывать неполные блоки (менее 4 байт)

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

Обратная операция требуется для вывода результата шифрования или хеширования в стандартный формат.

function bitArrayToUint8Array(bitArray) {
    const bitLen = bitArray[bitArray.length - 1];
    const byteLen = Math.ceil(bitLen / 8);

    const result = new Uint8Array(byteLen);

    let byteIndex = 0;

    for (let i = 0; i < bitArray.length - 1; i++) {
        const word = bitArray[i];

        result[byteIndex++] = (word >>> 24) & 0xff;
        if (byteIndex >= byteLen) break;

        result[byteIndex++] = (word >>> 16) & 0xff;
        if (byteIndex >= byteLen) break;

        result[byteIndex++] = (word >>> 8) & 0xff;
        if (byteIndex >= byteLen) break;

        result[byteIndex++] = word & 0xff;
        if (byteIndex >= byteLen) break;
    }

    return result;
}

Здесь важно учитывать:

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

Использование SJCL с WebCrypto и ArrayBuffer

Во многих случаях SJCL используется совместно с WebCrypto API. Например, для генерации случайных данных:

const randomBytes = crypto.getRandomValues(new Uint8Array(16));
const sjclData = uint8ArrayToBitArray(randomBytes);

И наоборот, при экспорте результата:

const encrypted = sjcl.encrypt("key", "message");
const parsed = JSON.parse(encrypted);
const ctBits = sjcl.codec.base64.toBits(parsed.ct);
const bytes = bitArrayToUint8Array(ctBits);

Кодеки SJCL и бинарные преобразования

SJCL предоставляет встроенные кодеки:

  • sjcl.codec.hex
  • sjcl.codec.base64
  • sjcl.codec.utf8String
  • sjcl.codec.bytes

Особенно важен bytes, который частично решает проблему взаимодействия с Uint8Array:

const bitArray = sjcl.codec.bytes.toBits(uint8Array);
const uint8 = sjcl.codec.bytes.fromBits(bitArray);

Однако реализация bytes не всегда оптимальна для больших объемов данных, так как выполняет дополнительные проверки и преобразования.


Производительность преобразований

Конвертация между Uint8Array и bitArray является одной из самых дорогих операций при использовании SJCL.

Основные узкие места:

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

Оптимизации:

  • минимизация количества преобразований
  • повторное использование буферов
  • работа напрямую с Uint8Array, если это возможно через sjcl.codec.bytes

Работа с потоковыми данными

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

Типичный подход:

async function processChunk(chunk) {
    const u8 = new Uint8Array(await chunk.arrayBuffer());
    const bits = sjcl.codec.bytes.toBits(u8);

    const encrypted = sjcl.encrypt("key", sjcl.codec.base64.fromBits(bits));
    return encrypted;
}

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


Обработка строковых данных через Uint8Array

Часто данные приходят в виде строки, но дальнейшая обработка требует бинарного представления.

function stringToUint8(str) {
    return new TextEncoder().encode(str);
}

function uint8ToString(u8) {
    return new TextDecoder().decode(u8);
}

Связка с SJCL:

const u8 = stringToUint8("секрет");
const bits = sjcl.codec.bytes.toBits(u8);
const hash = sjcl.hash.sha256.hash(bits);

Типичные ошибки при работе с бинарными форматами

  1. Игнорирование endianess SJCL использует big-endian порядок, тогда как многие Web API работают с little-endian.

  2. Потеря длины данных Отсутствие корректного учета последнего элемента bitArray приводит к обрезанию данных.

  3. Неправильное выравнивание байтов Особенно при длине, не кратной 4 байтам.

  4. Двойное кодирование Например, Base64 → UTF8 → bitArray вместо прямого преобразования.


Совместимость с Node.js Buffer

В Node.js основным бинарным типом является Buffer, который ближе к Uint8Array.

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

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

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

Buffer и Uint8Array совместимы на уровне памяти, что упрощает интеграцию SJCL в серверные приложения.


Практическая модель взаимодействия слоев данных

В реальных приложениях обычно используется цепочка:

  1. Источник данных (строка / файл / сеть)
  2. Uint8Array или ArrayBuffer
  3. преобразование в bitArray
  4. криптографическая операция SJCL
  5. преобразование обратно в Uint8Array
  6. кодирование (Base64 / Hex) для передачи

Эта модель позволяет изолировать криптографический слой от платформенных API.


Особенности работы с памятью

SJCL активно создаёт новые массивы при операциях с bitArray. Это приводит к:

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

Рациональная стратегия — минимизировать переходы между форматами и держать данные в одном представлении на протяжении операции.