Основная проблема клиентской криптографии в веб-приложениях заключается не в генерации ключей, а в их безопасном жизненном цикле: создании, использовании, хранении и уничтожении. Браузерная среда по своей природе не предоставляет постоянного защищённого хранилища, аналогичного аппаратным модулям безопасности на сервере, поэтому любая стратегия хранения ключевого материала должна учитывать модель угроз: XSS-атаки, компрометацию расширений, утечки через локальные API и доступ к данным со стороны пользователя или вредоносного скрипта.
Web Crypto API (crypto.subtle) предоставляет механизмы,
позволяющие минимизировать риски, но не устраняет их полностью. Ключевая
концепция заключается в том, что секретные ключи должны по возможности
оставаться неэкспортируемыми и использоваться только
внутри криптографического контекста браузера.
Базовый способ создания ключевого материала — использование
crypto.subtle.generateKey.
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256
},
true,
["encrypt", "decrypt"]
);
Второй параметр true/false определяет, можно ли
экспортировать ключ. Именно здесь закладывается фундамент
безопасности:
extractable: false — ключ нельзя извлечь в виде
raw-байтов или JSONextractable: true — ключ можно сохранить, но он
становится уязвимым при утечкеДля безопасного хранения критичных секретов используется:
extractable: false
Такой ключ невозможно сериализовать через exportKey, а
значит он не может быть украден через стандартные JS-инъекции, если не
происходит активное использование API в момент атаки.
Даже если ключ создан как неэкспортируемый, возникает практическая задача его сохранения между сессиями. Web Crypto API не предоставляет встроенного persistent storage для ключей. Это означает:
CryptoKey напрямую в удобном
виде (хотя поддержка частичная и зависит от браузера)Следовательно, безопасное хранение сводится к одной из стратегий:
IndexedDB — единственный стандартный механизм браузера, пригодный для
долговременного хранения бинарных данных. Однако хранение
CryptoKey имеет ограничения:
const db = indexedDB.open("crypto-db", 1);
Некоторые браузеры позволяют сохранять CryptoKey
напрямую, но это не универсально и не гарантируется спецификацией для
всех типов ключей.
Поэтому более надёжный подход — хранить зашифрованную версию ключа, а не сам ключ.
Web Crypto API поддерживает операции wrapKey и
unwrapKey, позволяющие шифровать ключи другим ключом.
const wrappedKey = await crypto.subtle.wrapKey(
"jwk",
dataKey,
wrappingKey,
{ name: "AES-GCM", iv: iv }
);
И восстановление:
const unwrappedKey = await crypto.subtle.unwrapKey(
"jwk",
wrappedKey,
wrappingKey,
{ name: "AES-GCM", iv: iv },
{
name: "AES-GCM"
},
false,
["encrypt", "decrypt"]
);
Даже если IndexedDB будет скомпрометирована, злоумышленник получит только зашифрованный ключ, но не сможет использовать его без KEK.
Один из наиболее распространённых подходов — генерация ключа из пользовательского пароля через PBKDF2.
const baseKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"]
);
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 310000,
hash: "SHA-256"
},
baseKey,
{
name: "AES-GCM",
length: 256
},
false,
["encrypt", "decrypt"]
);
Особенности:
Этот подход широко используется в end-to-end шифровании в веб-приложениях.
В ряде систем используется модель, при которой ключ:
let sessionKey;
async function initSessionKey() {
sessionKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
function clearKey() {
sessionKey = null;
}
Этот подход минимизирует поверхность атаки, но не решает проблему восстановления данных после перезагрузки.
Гибридные схемы часто используют RSA-OAEP или ECDH для защиты симметричных ключей.
Пример:
const encryptedKey = await crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
publicKey,
rawKeyBuffer
);
Такая схема используется в:
Даже при использовании Web Crypto API необходимо учитывать реальные векторы атак:
Наиболее критическая угроза. Если злоумышленник выполняет JS-код в контексте страницы:
crypto.subtle напрямуюНеэкспортируемость ключей не защищает от использования API.
Браузерные расширения с широкими правами могут:
Хотя Web Crypto API хранит ключи в изолированных объектах:
На практике используется комбинация подходов:
extractable: falseKEK выводится из пароля пользователя (PBKDF2)
DEK используется для данных
DEK хранится только в wrapped-виде
IndexedDB хранит:
Критически важным аспектом является уничтожение ключей:
sessionKey = null;
Однако этого недостаточно, поскольку:
Более строгие системы:
Поведение Web Crypto API может отличаться:
Поэтому переносимость требует:
crypto.subtle возможностейБезопасное хранение ключевого материала в браузере строится вокруг нескольких принципов:
localStorageCryptoKey через JSONВ браузерной криптографии безопасность хранения ключей не является свойством одного API. Она формируется архитектурой:
В результате безопасное хранение ключевого материала в браузере представляет собой не точку хранения, а управляемый жизненный цикл криптографических объектов внутри ограниченного и изолированного исполнения JavaScript.