Шифрование данных перед записью в localStorage

localStorage в браузере хранит данные в открытом виде, доступные любому скрипту, выполняющемуся в контексте страницы. Это создаёт прямую угрозу при хранении токенов, персональных данных или любой информации, чувствительной к утечке через XSS. Поэтому перед записью данных в localStorage используется симметричное шифрование на стороне клиента, где одним из практичных решений выступает Stanford JS Crypto Library (SJCL).

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

Ключевой принцип:

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

Общая архитектура работы с SJCL

SJCL реализует симметричное шифрование на базе AES с режимом CCM и встроенной генерацией случайных значений. Библиотека также поддерживает вывод ключей через PBKDF2.

Типовой поток выглядит следующим образом:

  1. Сериализация объекта в JSON
  2. Шифрование строки через SJCL
  3. Запись результата в localStorage
  4. Чтение строки
  5. Дешифрование
  6. Парсинг JSON

Базовое шифрование и запись в localStorage

SJCL предоставляет простой интерфейс:

const data = {
  userId: 42,
  token: "abc123",
  role: "admin"
};

const password = "strong-password";

// сериализация
const json = JSON.stringify(data);

// шифрование
const encrypted = sjcl.encrypt(password, json);

// запись в localStorage
localStorage.setItem("secure_data", encrypted);

Функция sjcl.encrypt возвращает JSON-строку, содержащую:

  • ciphertext
  • salt
  • IV (initialization vector)
  • параметры KDF

Это позволяет восстановить данные при наличии пароля.

Дешифрование данных

При чтении выполняется обратная операция:

const encrypted = localStorage.getItem("secure_data");

const decrypted = sjcl.decrypt(password, encrypted);

const data = JSON.parse(decrypted);

Если пароль неверный или данные повреждены, SJCL выбрасывает исключение. Поэтому в реальных приложениях необходимо оборачивать операцию в try/catch.

Обработка ошибок при расшифровке

let data = null;

try {
  const encrypted = localStorage.getItem("secure_data");
  if (encrypted) {
    const decrypted = sjcl.decrypt(password, encrypted);
    data = JSON.parse(decrypted);
  }
} catch (e) {
  console.error("Ошибка расшифровки данных:", e);
  localStorage.removeItem("secure_data");
}

Удаление повреждённых данных предотвращает повторные ошибки при последующих загрузках.

Шифрование сложных структур

SJCL работает со строками, поэтому любые структуры должны быть сериализованы заранее. Это включает вложенные объекты, массивы и конфигурации приложения.

const sessionState = {
  user: {
    id: 10,
    name: "Alex",
    permissions: ["read", "write"]
  },
  settings: {
    theme: "dark",
    lang: "ru"
  }
};

const encrypted = sjcl.encrypt(password, JSON.stringify(sessionState));
localStorage.setItem("session", encrypted);

При восстановлении важно строго соблюдать порядок операций:

const decrypted = sjcl.decrypt(password, localStorage.getItem("session"));
const sessionState = JSON.parse(decrypted);

Управление ключами шифрования

На практике пароль не должен быть жёстко зашит в код. SJCL позволяет использовать PBKDF2 для вывода ключа из пользовательского пароля.

const salt = sjcl.random.randomWords(4);

const key = sjcl.misc.pbkdf2(password, salt, 10000, 256);

Однако чаще используется встроенная схема sjcl.encrypt, которая автоматически:

  • генерирует salt
  • применяет PBKDF2
  • формирует ключ шифрования

Это снижает вероятность ошибок при ручной настройке криптографии.

Контроль параметров шифрования

SJCL позволяет управлять параметрами:

const encrypted = sjcl.encrypt(password, json, {
  iter: 10000,
  ks: 256,
  mode: "ccm",
  ts: 64
});

Здесь:

  • iter — число итераций PBKDF2 (влияет на устойчивость к brute-force)
  • ks — размер ключа
  • mode — режим блочного шифрования
  • ts — длина аутентификационного тега

Увеличение итераций повышает безопасность, но снижает производительность.

Поток безопасного хранения состояния приложения

При проектировании клиентского хранилища с шифрованием структура обычно строится вокруг двух функций:

function saveSecure(key, value, password) {
  const json = JSON.stringify(value);
  const encrypted = sjcl.encrypt(password, json);
  localStorage.setItem(key, encrypted);
}

function loadSecure(key, password) {
  const encrypted = localStorage.getItem(key);
  if (!encrypted) return null;

  const decrypted = sjcl.decrypt(password, encrypted);
  return JSON.parse(decrypted);
}

Такой подход унифицирует работу с любыми данными.

Особенности хранения и безопасности localStorage

localStorage имеет ряд критических ограничений:

  • доступен любому JS-коду в рамках origin
  • не защищён от XSS
  • не шифруется браузером
  • синхронный API блокирует поток выполнения

Поэтому даже при использовании SJCL остаётся риск компрометации при наличии вредоносного скрипта. Шифрование защищает данные только «в состоянии покоя», но не во время работы приложения.

Влияние XSS на модель защиты

Если атакующий получает возможность выполнять JavaScript в контексте страницы, он получает:

  • доступ к зашифрованным данным
  • доступ к паролю (если он хранится в памяти)
  • возможность перехватить данные до шифрования или после расшифровки

Поэтому SJCL в localStorage следует рассматривать как защиту от:

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

но не как защиту от XSS.

Оптимизация объёма данных

Шифрование увеличивает размер строки примерно на 30–100%. При больших объектах это важно учитывать.

Практики оптимизации:

  • хранение минимально необходимых данных
  • исключение дублируемых полей
  • сжатие JSON перед шифрованием (например, через структурные оптимизации)
const compact = {
  u: userId,
  t: token,
  r: role
};

Типичные ошибки при использовании SJCL с localStorage

  • хранение пароля рядом с зашифрованными данными
  • повторное использование одного IV вне стандартного API
  • попытка шифровать уже сериализованные JSON-строки без контроля формата
  • отсутствие обработки ошибок дешифрования
  • игнорирование размера localStorage (обычно ~5–10MB)

Сценарий сессионного хранения

Наиболее частый кейс — временное хранение состояния авторизации:

const session = {
  accessToken: "jwt-token",
  expires: Date.now() + 3600 * 1000
};

saveSecure("session", session, password);

При загрузке приложения выполняется проверка срока действия после расшифровки:

const session = loadSecure("session", password);

if (session && session.expires > Date.now()) {
  // активная сессия
} else {
  localStorage.removeItem("session");
}

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