Асинхронные операции и Web Workers

В библиотеке 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.

Ограничения основного потока

Основной поток браузера отвечает за:

  • обработку событий
  • рендеринг DOM
  • выполнение JavaScript
  • взаимодействие с пользователем

Любая синхронная криптография конкурирует с этими задачами. Даже 200–300 мс блокировки уже воспринимаются как лаг.

Поэтому любые операции, связанные с:

  • PBKDF2
  • bcrypt-подобными вычислениями (если реализованы поверх SJCL)
  • шифрованием больших массивов данных
  • массовой обработкой сообщений

требуют выноса из main thread.

Web Workers как изоляция криптографии

Web Workers позволяют выполнять JavaScript в отдельном потоке без доступа к DOM. Это делает их идеальной средой для SJCL.

Ключевая идея архитектуры:

  • основной поток отправляет задачу в worker
  • worker выполняет криптографию через SJCL
  • результат возвращается через postMessage

Базовая схема взаимодействия

main thread → worker (задача) worker → main thread (результат)

Подключение SJCL в Web Worker

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 напрямую.

Поэтому применяются кодеки:

  • Base64
  • Hex
  • JSON-строки

Преобразование данных

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 становится узким местом.

Используются два подхода:

1. Полный перенос в Worker

Весь файл отправляется в worker:

worker.postMessage({
    type: "encrypt",
    payload: {
        password,
        data: fileText
    }
});

Минус — копирование больших данных между потоками.

2. Разбиение на блоки

Данные делятся на части:

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.

Использование Web Worker для PBKDF2

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.

Работа с генератором случайных чисел в worker

SJCL использует пул энтропии, который наполняется событиями браузера. В Web Worker таких источников нет.

Поэтому:

  • sjcl.random в worker может быть менее надёжным
  • требуется предварительное насыщение энтропии в main thread

Пример:

sjcl.random.addEntropy(Math.random(), 32);

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

Архитектура изолированного криптографического сервиса

В сложных приложениях worker превращается в отдельный сервис:

  • очередь задач
  • приоритеты операций
  • кэширование результатов PBKDF2
  • повторное использование ключей

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

{
    id: 1,
    type: "encrypt",
    payload: {
        keyId: "user-key-1",
        data: "..."
    }
}

Worker может хранить промежуточное состояние:

const keyCache = new Map();

Асинхронная обёртка поверх SJCL

Для удобства создаётся 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
        });
    }
}

Типичные ошибки при использовании SJCL в Web Workers

  • попытка передать bitArray без сериализации
  • использование sjcl.random без подготовки энтропии
  • копирование больших строк вместо chunk-обработки
  • отсутствие контроля очереди задач
  • блокировка worker длительными синхронными циклами без разбиения

Асинхронные альтернативы без Web Workers

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

function asyncPbkd2(password, salt, iterations) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const result = sjcl.misc.pbkdf2(password, salt, iterations);
            resolve(result);
        }, 0);
    });
}

Однако этот подход не устраняет блокировку полностью, а лишь делит её на части.

Комбинирование SJCL с современными архитектурами

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

  • криптографический слой в worker
  • fallback для WebCrypto API
  • часть гибридной системы (SJCL + WebCrypto)

Web Worker становится границей между UI и криптографическим ядром, а SJCL — вычислительным модулем внутри этой изоляции.