Локальное хранилище в браузере (localStorage, IndexedDB, Cache Storage) не обеспечивает конфиденциальность данных. Любая информация, записанная без шифрования, может быть прочитана через консоль разработчика, вредоносные скрипты при XSS-уязвимостях или расширения браузера.
Web Crypto API предоставляет нативные криптографические примитивы для защиты данных на стороне клиента. Основной сценарий для практики безопасного хранения — симметричное шифрование с использованием AES-GCM, где ключ либо генерируется и хранится в памяти сессии, либо выводится из пароля пользователя.
Система безопасного хранения в браузере обычно строится по следующей схеме:
Критически важный момент: ключ никогда не должен храниться в открытом виде в localStorage.
Web Crypto API позволяет создать криптографически стойкий ключ:
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256
},
true,
["encrypt", "decrypt"]
);
Параметр extractable: true позволяет экспортировать ключ
при необходимости, но в защищённых сценариях предпочтительно
использовать false, чтобы исключить возможность утечки.
AES-GCM требует уникальный initialization vector (IV) для каждой операции шифрования. Он не является секретом, но обязан быть уникальным.
function generateIV() {
return crypto.getRandomValues(new Uint8Array(12));
}
async function encryptData(key, data) {
const iv = generateIV();
const encoded = new TextEncoder().encode(JSON.stringify(data));
const ciphertext = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
key,
encoded
);
return {
iv: Array.from(iv),
ciphertext: Array.from(new Uint8Array(ciphertext))
};
}
Результат шифрования преобразуется в массивы для хранения в JSON-формате.
После шифрования данные можно сохранять в браузере:
async function saveSecure(key, storageKey, data) {
const encrypted = await encryptData(key, data);
localStorage.setItem(storageKey, JSON.stringify(encrypted));
}
В таком виде данные бесполезны без ключа, даже если злоумышленник получит доступ к хранилищу.
Для восстановления данных необходимо извлечь IV и ciphertext и выполнить обратную операцию:
async function decryptData(key, encryptedObject) {
const iv = new Uint8Array(encryptedObject.iv);
const data = new Uint8Array(encryptedObject.ciphertext);
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv
},
key,
data
);
return JSON.parse(new TextDecoder().decode(decrypted));
}
async function secureStoreExample() {
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
const userData = {
token: "abc123",
role: "admin",
timestamp: Date.now()
};
await saveSecure(key, "secure-data", userData);
const stored = JSON.parse(localStorage.getItem("secure-data"));
const decrypted = await decryptData(key, stored);
return decrypted;
}
Генерация случайного ключа удобна для сессий, но не подходит для постоянного хранения. В таких случаях используется PBKDF2.
async function deriveKey(password, salt) {
const baseKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256"
},
baseKey,
{
name: "AES-GCM",
length: 256
},
false,
["encrypt", "decrypt"]
);
}
Соль (salt) должна быть случайной и сохраняться вместе с зашифрованными данными.
async function saveWithPassword(password, data) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = await deriveKey(password, salt);
const encrypted = await encryptData(key, data);
const payload = {
salt: Array.from(salt),
encrypted
};
localStorage.setItem("secure-box", JSON.stringify(payload));
}
async function loadWithPassword(password) {
const raw = JSON.parse(localStorage.getItem("secure-box"));
const salt = new Uint8Array(raw.salt);
const key = await deriveKey(password, salt);
return decryptData(key, raw.encrypted);
}
AES-GCM одновременно обеспечивает:
При изменении хотя бы одного байта ciphertext расшифровка завершится ошибкой.
Использование одного и того же IV с одним ключом полностью ломает безопасность AES-GCM. Каждый вызов шифрования обязан использовать уникальный IV.
Даже если данные зашифрованы, хранение ключа рядом полностью уничтожает смысл шифрования.
При PBKDF2 безопасность напрямую зависит от сложности пароля. Короткие или предсказуемые пароли легко подбираются.
Изменение структуры JSON без проверки может привести к ошибкам при расшифровке или потере данных.
localStorage ограничен по объёму и работает синхронно, что блокирует поток выполнения. IndexedDB предпочтительнее для больших объёмов зашифрованных данных.
Пример хранения:
function saveToIndexedDB(db, key, value) {
const tx = db.transaction(["secureStore"], "readwrite");
const store = tx.objectStore("secureStore");
store.put(value, key);
}
Шифрование остаётся идентичным, изменяется только слой хранения.
Криптография в браузере не защищает от выполнения вредоносного JavaScript. При наличии XSS атакующий может:
Поэтому Web Crypto API рассматривается как часть многоуровневой защиты, а не самостоятельное решение.
Все операции Web Crypto API построены на Promise-based интерфейсе. Это связано с тем, что криптографические вычисления выполняются вне основного потока.
Это влияет на архитектуру:
При интенсивной работе с шифрованием полезно:
Удобный формат для хранения зашифрованных данных:
{
version: 1,
algorithm: "AES-GCM",
iv: [...],
data: [...],
salt: [...]
}
Версионирование позволяет в будущем менять алгоритмы без потери совместимости.
Web Crypto API поддерживается всеми современными браузерами, но:
extractableТиповая структура модуля:
Такое разделение позволяет изолировать криптографию от бизнес-логики и упростить замену хранилища без изменения логики шифрования