Особенности локального хранилища браузера
localStorage представляет собой синхронное ключ-значение хранилище, доступное в браузере, которое сохраняет данные даже после закрытия вкладки или перезапуска браузера. Его основное назначение — хранение небольших объемов несекретной информации: настроек интерфейса, состояния UI, токенов с ограниченным временем жизни, кэша запросов.
Главное ограничение localStorage заключается в отсутствии встроенной защиты. Любое значение, записанное в него, доступно через JavaScript на стороне клиента. Это означает, что при наличии XSS-уязвимости или доступа к консоли браузера данные могут быть прочитаны напрямую. По этой причине хранение чувствительной информации требует дополнительного уровня защиты, и одним из наиболее распространённых решений становится симметричное шифрование с использованием Crypto-js.
Принципы защиты данных перед записью в localStorage
При работе с локальным хранилищем важно учитывать несколько базовых угроз:
Решение заключается в предварительном преобразовании данных в зашифрованный вид. На клиентской стороне это обычно реализуется через алгоритмы симметричного шифрования, где один и тот же ключ используется для шифрования и расшифровки.
Crypto-js предоставляет набор криптографических функций, включая AES (Advanced Encryption Standard), SHA-хэши, HMAC и PBKDF2 для генерации ключей.
Базовое шифрование данных с использованием AES
Наиболее часто используемый алгоритм в Crypto-js — AES. Он обеспечивает достаточный уровень стойкости при корректной реализации.
Пример базового шифрования строки:
import CryptoJS from "crypto-js";
const secretKey = "my-secret-key";
const data = "confidential information";
const encrypted = CryptoJS.AES.encrypt(data, secretKey).toString();
localStorage.setItem("payload", encrypted);
В этом случае Crypto-js самостоятельно генерирует необходимые параметры (включая IV), однако при таком подходе теряется контроль над процессом и усложняется совместимость между системами.
Расшифровка данных из localStorage
Для восстановления исходного значения используется тот же ключ:
import CryptoJS from "crypto-js";
const secretKey = "my-secret-key";
const encrypted = localStorage.getItem("payload");
const bytes = CryptoJS.AES.decrypt(encrypted, secretKey);
const originalText = bytes.toString(CryptoJS.enc.Utf8);
Результатом становится исходная строка, если ключ совпадает. При неправильном ключе или поврежденных данных результат будет пустым или некорректным.
Хранение объектов в зашифрованном виде
localStorage работает только со строками, поэтому объекты необходимо сериализовать перед шифрованием.
const userData = {
id: 42,
name: "Alex",
role: "admin"
};
const json = JSON.stringify(userData);
const encrypted = CryptoJS.AES.encrypt(json, secretKey).toString();
localStorage.setItem("user", encrypted);
При расшифровке выполняется обратное преобразование:
const encrypted = localStorage.getItem("user");
const bytes = CryptoJS.AES.decrypt(encrypted, secretKey);
const decryptedJson = bytes.toString(CryptoJS.enc.Utf8);
const userData = JSON.parse(decryptedJson);
Использование соли и производных ключей
Прямое использование строкового ключа снижает криптографическую устойчивость. Более корректный подход — генерация ключа через PBKDF2 с использованием соли.
const password = "user-password";
const salt = CryptoJS.lib.WordArray.random(16);
const key = CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 10000
});
Соль необходимо сохранять вместе с зашифрованными данными:
const encrypted = CryptoJS.AES.encrypt(data, key).toString();
const payload = {
salt: salt.toString(),
data: encrypted
};
localStorage.setItem("secure", JSON.stringify(payload));
Полный цикл шифрования и дешифрования с PBKDF2
Шифрование:
import CryptoJS from "crypto-js";
function encryptData(data, password) {
const salt = CryptoJS.lib.WordArray.random(16);
const key = CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 10000
});
const encrypted = CryptoJS.AES.encrypt(data, key).toString();
return JSON.stringify({
salt: salt.toString(),
data: encrypted
});
}
Дешифрование:
function decryptData(payload, password) {
const { salt, data } = JSON.parse(payload);
const key = CryptoJS.PBKDF2(password, CryptoJS.enc.Hex.parse(salt), {
keySize: 256 / 32,
iterations: 10000
});
const bytes = CryptoJS.AES.decrypt(data, key);
return bytes.toString(CryptoJS.enc.Utf8);
}
Режимы AES и влияние на безопасность
AES в Crypto-js поддерживает несколько режимов работы:
На практике CBC с уникальным IV считается базовым безопасным вариантом.
Пример явного задания режима:
const encrypted = CryptoJS.AES.encrypt(data, key, {
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
IV должен быть уникальным для каждого шифрования и сохраняться вместе с данными.
Хранение IV и структуры зашифрованного пакета
Корректная структура данных обычно включает:
Пример упаковки:
const iv = CryptoJS.lib.WordArray.random(16);
const encrypted = CryptoJS.AES.encrypt(data, key, { iv });
const package = {
iv: iv.toString(),
data: encrypted.toString()
};
localStorage.setItem("secureData", JSON.stringify(package));
Расшифровка:
const parsed = JSON.parse(localStorage.getItem("secureData"));
const iv = CryptoJS.enc.Hex.parse(parsed.iv);
const bytes = CryptoJS.AES.decrypt(parsed.data, key, { iv });
const result = bytes.toString(CryptoJS.enc.Utf8);
Типичные ошибки при использовании Crypto-js с localStorage
На практике встречаются повторяющиеся проблемы:
Каждая из этих ошибок снижает уровень защиты до формального.
Производительность и ограничения браузера
Crypto-js выполняет вычисления на JavaScript, что делает его менее производительным по сравнению с нативными Web Crypto API. При работе с большими объемами данных возможны:
Поэтому локальное шифрование целесообразно применять только для небольших объектов и конфигурационных данных.
Сравнение подхода Crypto-js и Web Crypto API
Web Crypto API предоставляет более низкоуровневый и производительный интерфейс, однако имеет ограничения:
Crypto-js выигрывает в простоте интеграции и переносимости кода, особенно в проектах без строгих требований к производительности.
Практическая архитектура хранения защищённых данных
Типичная схема работы с localStorage и Crypto-js включает несколько уровней:
При чтении:
Такая структура позволяет изолировать криптографию от бизнес-логики и минимизировать ошибки.
Безопасностные ограничения клиентского шифрования
Важно учитывать фундаментальный факт: любое клиентское шифрование не защищает от атак при полном контроле над окружением пользователя. Если злоумышленник может выполнить код в браузере, он может:
Поэтому использование Crypto-js в localStorage следует рассматривать как уровень защиты от случайного доступа и поверхностных атак, а не как полноценную криптографическую границу безопасности.