Web Crypto API Key Storage как альтернатива

Хранение криптографических ключей в браузерных приложениях всегда было одной из самых чувствительных частей архитектуры безопасности. В экосистеме JavaScript исторически использовались разные подходы: от полностью пользовательских реализаций (например, на базе SJCL) до стандартизированного Web Crypto API, встроенного в браузеры.

Stanford JavaScript Crypto Library (SJCL) изначально проектировалась как компактная криптографическая библиотека, работающая полностью в пользовательском пространстве JavaScript. Это означает, что управление ключами полностью контролируется приложением.

Типичный сценарий в SJCL выглядит следующим образом:

  • генерация ключа выполняется в JS
  • ключ хранится в виде строки или сериализованного объекта
  • шифрование выполняется напрямую через функции библиотеки
  • расшифровка требует наличия исходного ключа в памяти или в persistent storage

Пример базового подхода:

const password = "secret-password";
const data = "confidential text";

const encrypted = sjcl.encrypt(password, data);
const decrypted = sjcl.decrypt(password, encrypted);

Ключевой момент: пароль фактически становится криптографическим ключом через внутренние механизмы KDF (обычно PBKDF2).

Проблема такого подхода заключается в том, что:

  • ключи часто сериализуются в localStorage
  • защита зависит от логики приложения
  • нет аппаратной изоляции ключевого материала
  • любая XSS-уязвимость полностью компрометирует ключ

Архитектурная модель Web Crypto API

Web Crypto API реализует принципиально иной подход: ключи представляются как непрозрачные объекты (CryptoKey), доступ к которым ограничен браузером.

Основные особенности:

  • ключи могут быть неэкспортируемыми
  • криптографические операции выполняются внутри браузерного контекста
  • доступ к сырому материалу ключа может быть запрещён
  • поддерживается аппаратное ускорение (если доступно)

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

const key = await crypto.subtle.generateKey(
  {
    name: "AES-GCM",
    length: 256
  },
  true,
  ["encrypt", "decrypt"]
);

Здесь второй параметр true означает, что ключ можно экспортировать, но это не обязательно.

Хранение ключей в Web Crypto API

Самая важная концепция: CryptoKey нельзя напрямую сохранить в localStorage или IndexedDB без сериализации.

Для хранения используются два основных подхода:

Экспортируемые ключи

const raw = await crypto.subtle.exportKey("jwk", key);
localStorage.setItem("key", JSON.stringify(raw));

При восстановлении:

const parsed = JSON.parse(localStorage.getItem("key"));

const key = await crypto.subtle.importKey(
  "jwk",
  parsed,
  { name: "AES-GCM" },
  true,
  ["encrypt", "decrypt"]
);

Недостаток: ключ становится доступен в экспортируемом виде, что снижает безопасность до уровня хранения JSON.

Неэкспортируемые ключи

Более безопасный вариант:

const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

Такой ключ:

  • нельзя извлечь
  • нельзя сериализовать
  • можно использовать только через subtle операции

Для хранения применяется косвенный подход:

  • ключ хранится только в runtime (memory)
  • при перезагрузке создаётся заново или восстанавливается через derivation

Производные ключи и хранение паролей

Часто используется PBKDF2 или HKDF для восстановления ключей из пароля пользователя.

В Web Crypto API:

const enc = new TextEncoder();

const baseKey = await crypto.subtle.importKey(
  "raw",
  enc.encode(password),
  "PBKDF2",
  false,
  ["deriveKey"]
);

const derivedKey = await crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt: enc.encode("salt"),
    iterations: 100000,
    hash: "SHA-256"
  },
  baseKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

В SJCL аналогичный процесс выглядит проще:

const key = sjcl.misc.pbkdf2(password, salt, 100000, 256);

Разница принципиальная:

  • SJCL возвращает «сырой» массив байтов
  • Web Crypto API возвращает объект CryptoKey (если не экспортируемый — недоступен напрямую)

Сравнение моделей хранения ключей SJCL и Web Crypto API

1. Контроль над ключом

SJCL:

  • полный контроль на стороне JavaScript
  • ключ можно сериализовать, логировать, модифицировать

Web Crypto API:

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

2. Безопасность хранения

SJCL:

  • зависит от разработчика
  • часто используется localStorage
  • уязвим к XSS

Web Crypto API:

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

3. Производительность

SJCL:

  • чистый JS
  • медленнее на больших данных

Web Crypto API:

  • нативная реализация
  • аппаратное ускорение (AES-NI и аналоги)

4. Портируемость

SJCL:

  • одинаково работает везде

Web Crypto API:

  • зависит от браузера и окружения
  • ограниченная поддержка в старых средах

Миграция с SJCL на Web Crypto API

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

Шифрование

SJCL:

const ciphertext = sjcl.encrypt(password, data);

Web Crypto API:

const iv = crypto.getRandomValues(new Uint8Array(12));

const ciphertext = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  key,
  new TextEncoder().encode(data)
);

Дешифрование

SJCL:

const plaintext = sjcl.decrypt(password, ciphertext);

Web Crypto API:

const plaintext = await crypto.subtle.decrypt(
  { name: "AES-GCM", iv },
  key,
  ciphertext
);

Гибридная модель хранения ключей

На практике часто используется комбинированный подход:

  • SJCL применяется для совместимости или старых данных
  • Web Crypto API используется для новых операций
  • миграционный слой обеспечивает преобразование форматов

Пример стратегии:

  • старые данные → sjcl.decrypt
  • новые данные → crypto.subtle.encrypt
  • ключи пользователей → deriveKey через Web Crypto API
  • вспомогательные операции → SJCL (если требуется простота)

Проблемы и ограничения Web Crypto API в контексте хранения ключей

Несмотря на преимущества, модель имеет ограничения:

1. Отсутствие прямого доступа к ключам

Невозможность сериализовать неэкспортируемый ключ усложняет:

  • резервное копирование
  • перенос между устройствами
  • синхронизацию

2. Сложность архитектуры

В сравнении с SJCL:

  • больше асинхронного кода
  • больше сущностей (CryptoKey, AlgorithmIdentifier)
  • строгие требования к формату данных

3. Зависимость от браузерной реализации

Некоторые алгоритмы могут:

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

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

Наиболее устойчивый подход:

  • мастер-пароль пользователя

  • derivation через PBKDF2 / HKDF

  • генерация AES-GCM ключа через Web Crypto API

  • хранение:

    • либо в памяти
    • либо в IndexedDB (в экспортируемом виде)
  • использование неэкспортируемых ключей для критичных данных

SJCL в такой архитектуре чаще остаётся:

  • для обратной совместимости
  • для простых операций
  • как fallback слой в старых приложениях

Web Crypto API постепенно становится базовым стандартом хранения и использования ключей в браузере, вытесняя подходы, основанные на полностью управляемых JavaScript-библиотеках.