Хранение криптографических ключей в браузерных приложениях всегда было одной из самых чувствительных частей архитектуры безопасности. В экосистеме JavaScript исторически использовались разные подходы: от полностью пользовательских реализаций (например, на базе SJCL) до стандартизированного Web Crypto API, встроенного в браузеры.
Stanford JavaScript Crypto Library (SJCL) изначально проектировалась как компактная криптографическая библиотека, работающая полностью в пользовательском пространстве JavaScript. Это означает, что управление ключами полностью контролируется приложением.
Типичный сценарий в SJCL выглядит следующим образом:
Пример базового подхода:
const password = "secret-password";
const data = "confidential text";
const encrypted = sjcl.encrypt(password, data);
const decrypted = sjcl.decrypt(password, encrypted);
Ключевой момент: пароль фактически становится криптографическим ключом через внутренние механизмы KDF (обычно PBKDF2).
Проблема такого подхода заключается в том, что:
Web Crypto API реализует принципиально иной подход: ключи
представляются как непрозрачные объекты (CryptoKey), доступ
к которым ограничен браузером.
Основные особенности:
Пример генерации ключа:
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256
},
true,
["encrypt", "decrypt"]
);
Здесь второй параметр true означает, что ключ можно
экспортировать, но это не обязательно.
Самая важная концепция: 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 операцииДля хранения применяется косвенный подход:
Часто используется 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:
SJCL:
Web Crypto API:
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:
Некоторые алгоритмы могут:
Наиболее устойчивый подход:
мастер-пароль пользователя
derivation через PBKDF2 / HKDF
генерация AES-GCM ключа через Web Crypto API
хранение:
использование неэкспортируемых ключей для критичных данных
SJCL в такой архитектуре чаще остаётся:
Web Crypto API постепенно становится базовым стандартом хранения и использования ключей в браузере, вытесняя подходы, основанные на полностью управляемых JavaScript-библиотеках.