Ключевой объект Web Crypto API — CryptoKey — не является
«сырым» байтовым ключом в привычном смысле. Это управляемая средой
выполнения структура, привязанная к контексту безопасности браузера или
Worker’а. Именно поэтому управление памятью и кэшированием ключей
требует понимания его жизненного цикла.
CryptoKey может быть:
exportKey;Важно учитывать: даже если ключ выглядит как обычный объект, его внутреннее представление хранится в изолированном криптографическом контексте, который не подчиняется прямому управлению памяти JavaScript.
Кэширование ключей в памяти используется для минимизации дорогих
операций импорта (importKey) и генерации
(generateKey), особенно в высоконагруженных приложениях:
шифрование сообщений, токенизация, потоковая обработка данных.
Типовая схема кэша:
Пример структуры кэша:
const keyCache = new Map();
function getCacheKey(name, algo, usages) {
return `${name}:${algo}:${usages.join(',')}`;
}
Использование:
async function getKey() {
const cacheKey = getCacheKey("enc-key", "AES-GCM", ["encrypt", "decrypt"]);
if (keyCache.has(cacheKey)) {
return keyCache.get(cacheKey);
}
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
keyCache.set(cacheKey, key);
return key;
}
WeakMap позволяет привязать ключи к объектам без
препятствования сборщику мусора. Это полезно при ассоциации
CryptoKey с DOM-объектами, сессиями или временными
контекстами.
const sessionKeyMap = new WeakMap();
function attachKey(session, key) {
sessionKeyMap.set(session, key);
}
function getKey(session) {
return sessionKeyMap.get(session);
}
Преимущество:
session.Использование обычного Map без политики очистки приводит
к накоплению CryptoKey, особенно при динамическом создании
ключей (например, per-user, per-tab, per-request).
Типичные причины утечек:
Практический подход — ограниченный по размеру LRU-кэш (Least Recently Used):
class LRUCache {
constructor(limit = 50) {
this.limit = limit;
this.map = new Map();
}
get(key) {
if (!this.map.has(key)) return null;
const value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value);
return value;
}
set(key, value) {
if (this.map.has(key)) {
this.map.delete(key);
}
this.map.set(key, value);
if (this.map.size > this.limit) {
const oldest = this.map.keys().next().value;
this.map.delete(oldest);
}
}
}
LRU особенно важен при:
CryptoKey можно сохранять через:
Экспортируемые ключи:
const raw = await crypto.subtle.exportKey("raw", key);
Сохранение в IndexedDB:
const db = indexedDB.open("crypto-db", 1);
store.put(key, "aes-key-1");
Особенность: при восстановлении ключа необходимо использовать
importKey, что делает кэширование особенно важным.
В реальных приложениях ключи делятся на уровни доступа:
Горячие (hot keys):
Холодные (cold keys):
importKey.Переход:
async function resolveKey(id) {
const cached = keyCache.get(id);
if (cached) return cached;
const stored = await idbGet(id);
const key = await crypto.subtle.importKey(
"raw",
stored,
"AES-GCM",
true,
["encrypt", "decrypt"]
);
keyCache.set(id, key);
return key;
}
Web Crypto API не предоставляет встроенного TTL, поэтому управление сроком жизни реализуется вручную.
Подходы:
const cache = new Map();
function setKey(id, key) {
cache.set(id, {
key,
ts: Date.now()
});
}
setInterval(() => {
const now = Date.now();
for (const [id, entry] of cache) {
if (now - entry.ts > 300000) {
cache.delete(id);
}
}
}, 60000);
CryptoKey:
Практический вывод:
Кэширование криптографических ключей всегда повышает риск:
Меры снижения риска:
Для сложных приложений (например, SPA с мультисессиями) кэш должен быть изолирован:
class KeyStore {
constructor() {
this.cache = new Map();
}
set(sessionId, key) {
this.cache.set(sessionId, key);
}
get(sessionId) {
return this.cache.get(sessionId);
}
clearSession(sessionId) {
this.cache.delete(sessionId);
}
}
В Worker-среде это особенно важно, так как память разделена логически, но не физически между задачами.
Переизбыток ключей часто возникает из-за:
Рациональная стратегия:
Типичная зрелая схема выглядит так:
Такая модель снижает: