В современных веб-приложениях криптография почти всегда работает с
бинарными данными. Браузерные API, WebCrypto, файловые API и сетевые
протоколы оперируют ArrayBuffer и Uint8Array,
тогда как SJCL использует собственное представление —
bitArray. Именно на стыке этих двух миров чаще всего
возникают ошибки, потери данных и неоптимальные преобразования.
В основе SJCL лежит структура bitArray — массив
32-битных слов с дополнительным элементом длины. Это не байтовый массив
в привычном смысле, а логическое представление битовой
последовательности.
Ключевая особенность:
Пример структуры:
[0x61626300, 0x00000018] // строка "abc" в битовом представлении
Такой формат удобен для криптографических операций, но несовместим напрямую с Web API.
В браузерной и 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 не работает напрямую с такими структурами, поэтому требуется слой преобразования.
Одна из наиболее частых операций — преобразование входных данных из 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;
}
Ключевые моменты:
Обратная операция требуется для вывода результата шифрования или хеширования в стандартный формат.
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 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.codec.hexsjcl.codec.base64sjcl.codec.utf8Stringsjcl.codec.bytesОсобенно важен bytes, который частично решает проблему
взаимодействия с Uint8Array:
const bitArray = sjcl.codec.bytes.toBits(uint8Array);
const uint8 = sjcl.codec.bytes.fromBits(bitArray);
Однако реализация bytes не всегда оптимальна для больших
объемов данных, так как выполняет дополнительные проверки и
преобразования.
Конвертация между Uint8Array и bitArray
является одной из самых дорогих операций при использовании SJCL.
Основные узкие места:
Оптимизации:
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 не поддерживает стриминг нативно, поэтому приходится разбивать данные на блоки.
Часто данные приходят в виде строки, но дальнейшая обработка требует бинарного представления.
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);
Игнорирование endianess SJCL использует big-endian порядок, тогда как многие Web API работают с little-endian.
Потеря длины данных Отсутствие корректного учета последнего
элемента bitArray приводит к обрезанию данных.
Неправильное выравнивание байтов Особенно при длине, не кратной 4 байтам.
Двойное кодирование Например, Base64 → UTF8 → bitArray вместо прямого преобразования.
В 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 в серверные приложения.
В реальных приложениях обычно используется цепочка:
Uint8Array или ArrayBufferbitArrayUint8ArrayЭта модель позволяет изолировать криптографический слой от платформенных API.
SJCL активно создаёт новые массивы при операциях с
bitArray. Это приводит к:
Рациональная стратегия — минимизировать переходы между форматами и держать данные в одном представлении на протяжении операции.