В библиотеке SJCL большинство криптографических операций реализовано синхронно и выполняется в одном потоке JavaScript. Это упрощает использование, но создаёт серьёзные ограничения при работе с ресурсоёмкими алгоритмами: PBKDF2 с большим числом итераций, шифрование больших объёмов данных, генерация ключей и операции, связанные с хэшированием.
Любая длительная операция блокирует основной поток браузера, из-за чего интерфейс перестаёт реагировать, а события и отрисовка кадров задерживаются. Именно поэтому в реальных приложениях SJCL почти всегда комбинируется с Web Workers или асинхронными обёртками.
Базовый дизайн библиотеки ориентирован на предсказуемость и минимальную зависимость от окружения. Например:
sjcl.encrypt() выполняется полностью синхронноsjcl.decrypt() также блокирует поток до завершенияsjcl.misc.pbkdf2() может работать десятки или сотни
миллисекунд при высоких параметрах итерацийsjcl.random зависит от состояния
пула энтропииВсё это означает, что при увеличении сложности параметров криптографии интерфейс начинает «замерзать».
Особенно критичны операции вида:
const derivedKey = sjcl.misc.pbkdf2(password, salt, 100000, 256);
При 100k+ итераций выполнение может занять заметное время, полностью блокируя UI.
Основной поток браузера отвечает за:
Любая синхронная криптография конкурирует с этими задачами. Даже 200–300 мс блокировки уже воспринимаются как лаг.
Поэтому любые операции, связанные с:
требуют выноса из main thread.
Web Workers позволяют выполнять JavaScript в отдельном потоке без доступа к DOM. Это делает их идеальной средой для SJCL.
Ключевая идея архитектуры:
postMessagemain thread → worker (задача) worker → main thread (результат)
Worker-файл должен содержать саму библиотеку SJCL или её сборку.
Пример worker.js:
importScripts("sjcl.js");
self.onmess age = function (e) {
const { id, type, payload } = e.data;
try {
let result;
if (type === "pbkdf2") {
result = sjcl.misc.pbkdf2(
payload.password,
payload.salt,
payload.iterations,
payload.keySize
);
}
if (type === "encrypt") {
result = sjcl.encrypt(payload.password, payload.data);
}
if (type === "decrypt") {
result = sjcl.decrypt(payload.password, payload.data);
}
self.postMessage({
id,
result: result
});
} catch (err) {
self.postMessage({
id,
error: err.message
});
}
};
При работе с worker важно иметь систему идентификаторов задач, чтобы сопоставлять ответы.
const worker = new Worker("worker.js");
let counter = 0;
const callbacks = new Map();
worker.onmess age = function (e) {
const { id, result, error } = e.data;
const cb = callbacks.get(id);
if (!cb) return;
callbacks.delete(id);
if (error) cb.reject(error);
else cb.resolve(result);
};
function runTask(type, payload) {
const id = counter++;
return new Promise((resolve, reject) => {
callbacks.set(id, { resolve, reject });
worker.postMessage({
id,
type,
payload
});
});
}
Теперь криптографические операции становятся асинхронными:
const key = await runTask("pbkdf2", {
password: "secret",
salt: "random_salt",
iterations: 100000,
keySize: 256
});
SJCL использует внутренний формат bitArray, который не
может быть передан через postMessage напрямую.
Поэтому применяются кодеки:
const base64 = sjcl.codec.base64.fromBits(bits);
const bitsBack = sjcl.codec.base64.toBits(base64);
При работе через worker предпочтительно сериализовать данные заранее:
const encrypted = sjcl.encrypt(password, data);
const serialized = JSON.stringify(encrypted);
И наоборот:
const parsed = JSON.parse(serialized);
const decrypted = sjcl.decrypt(password, parsed);
При обработке больших массивов (например, файлов) синхронный вызов
sjcl.encrypt становится узким местом.
Используются два подхода:
Весь файл отправляется в worker:
worker.postMessage({
type: "encrypt",
payload: {
password,
data: fileText
}
});
Минус — копирование больших данных между потоками.
Данные делятся на части:
const chunkSize = 64 * 1024;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
worker.postMessage({
type: "encryptChunk",
payload: chunk
});
}
Worker собирает результат постепенно, применяя режимы вроде CBC или CTR.
PBKDF2 — одна из самых тяжёлых функций в SJCL при высоких итерациях.
В worker:
if (type === "pbkdf2") {
const { password, salt, iterations, keySize } = payload;
const derived = sjcl.misc.pbkdf2(
password,
salt,
iterations,
keySize
);
result = sjcl.codec.hex.fromBits(derived);
}
Возврат в hex позволяет избежать проблем с сериализацией
bitArray.
SJCL использует пул энтропии, который наполняется событиями браузера. В Web Worker таких источников нет.
Поэтому:
sjcl.random в worker может быть менее надёжнымПример:
sjcl.random.addEntropy(Math.random(), 32);
После этого можно безопасно использовать генерацию внутри worker, но с осторожностью к криптографическим требованиям.
В сложных приложениях worker превращается в отдельный сервис:
Пример структуры сообщений:
{
id: 1,
type: "encrypt",
payload: {
keyId: "user-key-1",
data: "..."
}
}
Worker может хранить промежуточное состояние:
const keyCache = new Map();
Для удобства создаётся API:
class SjclCrypto {
constructor(worker) {
this.worker = worker;
this.counter = 0;
this.callbacks = new Map();
worker.onmess age = (e) => {
const { id, result, error } = e.data;
const cb = this.callbacks.get(id);
if (!cb) return;
this.callbacks.delete(id);
error ? cb.reject(error) : cb.resolve(result);
};
}
exec(type, payload) {
const id = this.counter++;
return new Promise((resolve, reject) => {
this.callbacks.set(id, { resolve, reject });
this.worker.postMessage({ id, type, payload });
});
}
encrypt(password, data) {
return this.exec("encrypt", { password, data });
}
decrypt(password, data) {
return this.exec("decrypt", { password, data });
}
pbkdf2(password, salt, iterations, keySize) {
return this.exec("pbkdf2", {
password,
salt,
iterations,
keySize
});
}
}
bitArray без сериализацииsjcl.random без подготовки энтропииЕсли workers недоступны, используется кооперативная асинхронность:
function asyncPbkd2(password, salt, iterations) {
return new Promise((resolve) => {
setTimeout(() => {
const result = sjcl.misc.pbkdf2(password, salt, iterations);
resolve(result);
}, 0);
});
}
Однако этот подход не устраняет блокировку полностью, а лишь делит её на части.
В современных приложениях SJCL часто используется как:
Web Worker становится границей между UI и криптографическим ядром, а SJCL — вычислительным модулем внутри этой изоляции.