Очистка чувствительных данных из памяти

В среде JavaScript отсутствует детерминированное управление памятью. Сборщик мусора может перемещать, копировать и освобождать объекты в любой момент времени, что делает невозможным строгую гарантию физического стирания данных из памяти. Это критически важно при работе с криптографическими примитивами, где ключи, пароли и промежуточные значения должны быть минимально экспонированы.

Stanford JS Crypto Library (SJCL) предоставляет структуры данных и утилиты, позволяющие уменьшить поверхность утечки, однако ответственность за очистку чувствительных данных остаётся на разработчике.


Представление данных в SJCL и точки риска

SJCL использует несколько внутренних представлений данных:

  • bitArray — основной формат для криптографических операций
  • числовые массивы (JavaScript Array) — промежуточные буферы
  • строки UTF-8 — при кодировании и декодировании
  • BigNumber (sjcl.bn) — для математических операций в асимметричной криптографии

Каждый из этих типов имеет различную степень управляемости в контексте очистки памяти.

Наиболее безопасной единицей для последующей очистки является bitArray, так как он представляет собой обычный массив чисел (32-битные слова), которые можно перезаписать.


Проблема неизменяемых строк

Строки в JavaScript неизменяемы. Это означает:

  • невозможно перезаписать строку в памяти
  • любая операция “очистки” создаёт новую строку, не затрагивая старую
  • оригинальное значение остаётся в памяти до сборки мусора

Пример проблемного сценария:

var password = "super_secret_password";
var bits = sjcl.codec.utf8String.toBits(password);

После этого строка password может оставаться в памяти неопределённое время.

Рекомендация заключается в минимизации времени жизни строковых секретов и немедленном преобразовании их в bitArray.


Очистка bitArray после использования

bitArray в SJCL — это массив 32-битных целых чисел. Его можно перезаписать вручную.

Базовая стратегия очистки

function zeroizeBitArray(arr) {
    if (!arr) return;

    for (var i = 0; i < arr.length; i++) {
        arr[i] = 0;
    }
}

После выполнения криптографической операции:

var keyBits = sjcl.codec.utf8String.toBits("secret_key");

var encrypted = sjcl.encrypt("password", "data", keyBits);

// очистка
zeroizeBitArray(keyBits);

Важно учитывать, что sjcl.encrypt может создавать внутренние копии ключа, которые также невозможно полностью контролировать.


Очистка объектов sjcl.bn (BigNumber)

sjcl.bn используется в RSA, DSA и других асимметричных схемах. Эти объекты содержат массивы слов, представляющие большие числа.

Основные методы управления состоянием:

  • set() — перезапись значения
  • add(), mul() — создают новые состояния
  • внутренние массивы слов

Очистка может быть выполнена через принудительное обнуление:

function clearBigNumber(bn) {
    if (!bn) return;

    // приведение к нулю
    bn = bn.set(0);

    // попытка очистки внутренних слов
    if (bn.limbs) {
        for (var i = 0; i < bn.limbs.length; i++) {
            bn.limbs[i] = 0;
        }
    }
}

Следует учитывать, что структура limbs не является частью публичного API и может изменяться между версиями SJCL.


Очистка временных буферов в криптографических операциях

При работе с SJCL часто создаются промежуточные значения:

  • ключи сеансов
  • IV (initialization vector)
  • HMAC-буферы
  • результат PBKDF2

Пример генерации ключа:

var salt = sjcl.random.randomWords(4);
var derivedKey = sjcl.misc.pbkdf2("password", salt, 10000, 256);

После использования необходимо очистить оба массива:

zeroizeBitArray(salt);
zeroizeBitArray(derivedKey);

Особенно важно учитывать PBKDF2: результат может быть скопирован внутри реализации, и полная очистка невозможна, но внешние ссылки должны быть уничтожены.


Уничтожение ссылок и управление жизненным циклом объектов

Очистка памяти в JavaScript во многом сводится к разрыву ссылок:

var sessionKey = sjcl.random.randomWords(8);

// использование
var mac = sjcl.misc.hmac(sessionKey);

// разрыв ссылки
sessionKey = null;

Удаление ссылки увеличивает вероятность того, что сборщик мусора освободит память, однако не гарантирует моментального удаления.


Опасность копирования данных внутри SJCL

SJCL часто копирует данные внутри функций:

  • при шифровании создаются промежуточные bitArray
  • HMAC дублирует ключ
  • AES расширяет ключи расписания

Пример:

var key = sjcl.codec.hex.toBits("00112233445566778899aabbccddeeff");
var aes = new sjcl.cipher.aes(key);

После этого оригинальный key может быть очищен, но внутри aes уже существует expanded key schedule, который хранится отдельно.

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


Подход “минимального времени жизни секретов”

Практический подход заключается не в попытке полного удаления, а в минимизации времени существования секретов:

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

Пример правильного локального использования:

function encryptMessage(msg, password) {
    var key = sjcl.codec.utf8String.toBits(password);

    var result = sjcl.encrypt(key, msg);

    zeroizeBitArray(key);

    return result;
}

Работа с sjcl.random и потенциальные утечки

sjcl.random используется для генерации криптографически стойких чисел. Внутренний пул энтропии также содержит чувствительные данные.

Однако прямой доступ к нему ограничен, и очистка пула не предусмотрена публичным API.

Рекомендуемая модель поведения:

  • не извлекать внутренние состояния генератора
  • не хранить seed явно
  • полагаться на автоматическое управление энтропией

Практика безопасного обнуления структур

Обобщённый шаблон очистки:

function secureWipe(obj) {
    if (!obj) return;

    if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; i++) {
            obj[i] = 0;
        }
        obj.length = 0;
        return;
    }

    if (typeof obj === "object") {
        for (var k in obj) {
            if (obj.hasOwnProperty(k)) {
                obj[k] = null;
            }
        }
    }
}

Использование:

var session = {
    key: sjcl.random.randomWords(8),
    iv: sjcl.random.randomWords(4)
};

secureWipe(session.key);
secureWipe(session.iv);
session = null;

Ограничения модели памяти JavaScript

Даже при строгом контроле:

  • JIT-компилятор может копировать значения
  • garbage collector может задерживать освобождение
  • движок может хранить оптимизированные копии строк и массивов
  • регистры CPU могут содержать данные временно

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


Вывод практических правил работы с SJCL и памятью

  • использовать bitArray как основной формат секретов
  • избегать хранения паролей в строках дольше минимально необходимого времени
  • перезаписывать массивы вручную после использования
  • обнулять временные ключи, salt и IV
  • разрывать ссылки на криптографические объекты
  • учитывать внутренние копирования данных библиотекой
  • не полагаться на сборщик мусора как на механизм очистки секретов