Криптографические ключи в веб-приложениях существуют в условиях, где отсутствует физический контроль над средой исполнения. Браузерное окружение одновременно предоставляет удобство и расширяемую поверхность атак: XSS, вредоносные расширения, компрометация зависимостей, подмена CDN. В таких условиях выбор стратегии хранения приватных ключей определяет реальную стойкость всей криптосистемы на стороне клиента.
Библиотеки уровня TweetNaCl.js и nacl.js не решают задачу хранения — они предоставляют примитивы. Управление ключами остаётся задачей архитектуры приложения.
Подход memory-only предполагает, что приватные ключи существуют только в оперативной памяти JavaScript-контекста и никогда не записываются в долговременные хранилища браузера.
import nacl from "tweetnacl";
const keyPair = nacl.box.keyPair();
// приватный ключ существует только в памяти
const privateKey = keyPair.secretKey;
const publicKey = keyPair.publicKey;
В этом режиме любая попытка сохранения ключа в
localStorage или IndexedDB сознательно исключается.
IndexedDB применяется для хранения криптографических артефактов, но сам по себе не обеспечивает защиты. Это только структурированное хранилище, доступное из JavaScript.
Любые данные в IndexedDB:
Поэтому приватные ключи нельзя хранить в открытом виде.
Практический паттерн — key wrapping: приватный ключ хранится только в зашифрованном виде.
Web Crypto API предоставляет нативные примитивы для PBKDF2, AES-GCM и RSA-OAEP (в зависимости от браузера).
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const baseKey = await crypto.subtle.importKey(
"raw",
enc.encode(password),
"PBKDF2",
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 150000,
hash: "SHA-256"
},
baseKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
async function encryptPrivateKey(privateKey, aesKey) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
aesKey,
privateKey
);
return { iv, encrypted };
}
async function saveKey(db, id, payload) {
const tx = db.transaction("keys", "readwrite");
const store = tx.objectStore("keys");
store.put({
id,
iv: payload.iv,
data: payload.encrypted
});
return tx.complete;
}
async function decryptPrivateKey(encrypted, iv, aesKey) {
return crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
aesKey,
encrypted
);
}
После восстановления ключ можно передать в TweetNaCl.js:
const keyPair = nacl.box.keyPair.fromSecretKey(new Uint8Array(privateKey));
Даже при использовании Web Crypto API злоумышленник с доступом к DOM может:
Расширения имеют доступ к контексту страницы и могут:
Практический подход — минимизация доступности ключа:
Пример через Worker:
// main thread
const worker = new Worker("crypto-worker.js");
worker.postMessage({ type: "init", password });
Часто используется комбинированная модель:
Это снижает число операций с диском и уменьшает время существования уязвимого состояния.
TweetNaCl.js не имеет встроенного менеджера ключей. Вся работа строится вокруг:
nacl.box.keyPair()nacl.sign.keyPair()Это означает:
Выбор стратегии зависит от требований:
Независимо от выбранного подхода: