Шифрование данных в localStorage

Особенности локального хранилища браузера

localStorage представляет собой синхронное ключ-значение хранилище, доступное в браузере, которое сохраняет данные даже после закрытия вкладки или перезапуска браузера. Его основное назначение — хранение небольших объемов несекретной информации: настроек интерфейса, состояния UI, токенов с ограниченным временем жизни, кэша запросов.

Главное ограничение localStorage заключается в отсутствии встроенной защиты. Любое значение, записанное в него, доступно через JavaScript на стороне клиента. Это означает, что при наличии XSS-уязвимости или доступа к консоли браузера данные могут быть прочитаны напрямую. По этой причине хранение чувствительной информации требует дополнительного уровня защиты, и одним из наиболее распространённых решений становится симметричное шифрование с использованием Crypto-js.


Принципы защиты данных перед записью в localStorage

При работе с локальным хранилищем важно учитывать несколько базовых угроз:

  • доступ к данным через сторонние скрипты при XSS
  • утечка данных через расширения браузера
  • невозможность контроля серверной стороной
  • отсутствие встроенного шифрования

Решение заключается в предварительном преобразовании данных в зашифрованный вид. На клиентской стороне это обычно реализуется через алгоритмы симметричного шифрования, где один и тот же ключ используется для шифрования и расшифровки.

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 (Cipher Block Chaining) — используется по умолчанию
  • ECB — не рекомендуется из-за предсказуемости
  • CFB, OFB, CTR — применяются в специфических сценариях

На практике CBC с уникальным IV считается базовым безопасным вариантом.

Пример явного задания режима:

const encrypted = CryptoJS.AES.encrypt(data, key, {
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
}).toString();

IV должен быть уникальным для каждого шифрования и сохраняться вместе с данными.


Хранение IV и структуры зашифрованного пакета

Корректная структура данных обычно включает:

  • зашифрованный текст
  • IV (инициализационный вектор)
  • соль (если используется PBKDF2)

Пример упаковки:

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

На практике встречаются повторяющиеся проблемы:

  • использование фиксированного ключа без деривации
  • отсутствие IV или его переиспользование
  • хранение ключа в том же localStorage
  • отсутствие обработки ошибок дешифрования
  • шифрование без сериализации JSON

Каждая из этих ошибок снижает уровень защиты до формального.


Производительность и ограничения браузера

Crypto-js выполняет вычисления на JavaScript, что делает его менее производительным по сравнению с нативными Web Crypto API. При работе с большими объемами данных возможны:

  • задержки UI при синхронном шифровании
  • блокировка основного потока
  • рост времени загрузки страницы при дешифровке

Поэтому локальное шифрование целесообразно применять только для небольших объектов и конфигурационных данных.


Сравнение подхода Crypto-js и Web Crypto API

Web Crypto API предоставляет более низкоуровневый и производительный интерфейс, однако имеет ограничения:

  • асинхронная модель
  • более сложная реализация
  • ограниченная поддержка старых браузеров

Crypto-js выигрывает в простоте интеграции и переносимости кода, особенно в проектах без строгих требований к производительности.


Практическая архитектура хранения защищённых данных

Типичная схема работы с localStorage и Crypto-js включает несколько уровней:

  • пользовательский ввод или получение данных
  • сериализация объекта
  • генерация ключа (статического или производного)
  • шифрование AES
  • упаковка IV и соли
  • запись в localStorage

При чтении:

  • извлечение строки
  • парсинг структуры
  • восстановление ключа
  • расшифровка
  • десериализация JSON

Такая структура позволяет изолировать криптографию от бизнес-логики и минимизировать ошибки.


Безопасностные ограничения клиентского шифрования

Важно учитывать фундаментальный факт: любое клиентское шифрование не защищает от атак при полном контроле над окружением пользователя. Если злоумышленник может выполнить код в браузере, он может:

  • перехватить ключ до шифрования
  • изменить алгоритм расшифровки
  • подменить данные до сохранения

Поэтому использование Crypto-js в localStorage следует рассматривать как уровень защиты от случайного доступа и поверхностных атак, а не как полноценную криптографическую границу безопасности.