Шифрование данных перед сохранением

Роль шифрования в клиентском хранилище

Клиентское хранилище в браузере по своей природе не является защищённым. Любые данные, сохранённые через IndexedDB, WebSQL или localStorage, доступны через инструменты разработчика, расширения браузера и потенциально вредоносные скрипты, внедрённые в контекст страницы. Даже при использовании абстракции вроде localForage уровень защиты не меняется — библиотека лишь унифицирует API поверх разных движков хранения.

Шифрование перед сохранением решает одну ключевую задачу: превращает данные в форму, непригодную для чтения без знания ключа. Это особенно важно при хранении:

  • персональных данных пользователей;
  • токенов доступа (с ограничениями);
  • локальных кэшей API с чувствительной информацией;
  • бизнес-данных, которые не должны быть доступны из DevTools.

При этом важно учитывать, что шифрование на клиенте защищает не от всех угроз. Оно не предотвращает кражу ключа, XSS-атаки или выполнение вредоносного кода в контексте страницы, но существенно усложняет прямое чтение данных из хранилища.


Архитектура слоя шифрования поверх localForage

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

  • setItem(key, value)
  • getItem(key)
  • removeItem(key)
  • clear()

Шифрование внедряется как промежуточный слой между приложением и хранилищем.

Типовая схема:

  1. Данные сериализуются в строку или бинарный формат.
  2. Выполняется шифрование.
  3. Результат сохраняется через localForage.
  4. При чтении происходит обратное преобразование.

Абстракция может быть реализована как обёртка:

  • secureSetItem
  • secureGetItem

или как расширенный сервис хранения.


Выбор алгоритма шифрования

В браузерной среде стандартом де-факто является Web Crypto API. Он обеспечивает аппаратно ускоренное шифрование и безопасную работу с ключами.

На практике применяются следующие алгоритмы:

AES-GCM

Наиболее распространённый вариант:

  • симметричное шифрование;
  • встроенная проверка целостности (authentication tag);
  • высокая производительность;
  • поддержка большинством браузеров.

Ключевая особенность — наличие IV (initialization vector), который должен быть уникальным для каждой операции шифрования.

PBKDF2 / HKDF для получения ключей

Если ключ формируется из пароля пользователя, требуется деривация:

  • PBKDF2 используется для повышения стойкости пароля;
  • HKDF применяется для производных ключей в протоколах.

Формат хранения зашифрованных данных

localForage может хранить строки, объекты, массивы и бинарные данные. При шифровании чаще всего используется сериализация в JSON-структуру:

{
  "iv": "...",
  "ciphertext": "...",
  "salt": "...",
  "version": 1
}

Каждое поле выполняет роль:

  • iv — уникальный вектор инициализации;
  • ciphertext — зашифрованные данные;
  • salt — соль для ключа (если используется пароль);
  • version — версия схемы шифрования для миграций.

Базовая реализация шифрования с Web Crypto API

Шифрование через AES-GCM:

async function encryptData(key, data) {
  const encoded = new TextEncoder().encode(JSON.stringify(data));
  const iv = crypto.getRandomValues(new Uint8Array(12));

  const encrypted = await crypto.subtle.encrypt(
    {
      name: "AES-GCM",
      iv
    },
    key,
    encoded
  );

  return {
    iv: Array.from(iv),
    ciphertext: Array.from(new Uint8Array(encrypted))
  };
}

Дешифрование:

async function decryptData(key, payload) {
  const iv = new Uint8Array(payload.iv);
  const ciphertext = new Uint8Array(payload.ciphertext);

  const decrypted = await crypto.subtle.decrypt(
    {
      name: "AES-GCM",
      iv
    },
    key,
    ciphertext
  );

  return JSON.parse(new TextDecoder().decode(decrypted));
}

Интеграция с localForage

Шифрующий слой оборачивает стандартные операции:

import localForage from "localforage";

class SecureStorage {
  constructor(cryptoKey) {
    this.key = cryptoKey;
  }

  async setItem(key, value) {
    const encrypted = await encryptData(this.key, value);
    return localForage.setItem(key, encrypted);
  }

  async getItem(key) {
    const encrypted = await localForage.getItem(key);
    if (!encrypted) return null;
    return decryptData(this.key, encrypted);
  }

  async removeItem(key) {
    return localForage.removeItem(key);
  }
}

Такой подход сохраняет все преимущества localForage:

  • единый API;
  • асинхронность;
  • кросс-драйверность;
  • fallback между IndexedDB и другими механизмами.

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

Наиболее сложный аспект — не само шифрование, а хранение ключа.

Варианты хранения ключа:

  1. В памяти приложения

    • ключ существует только во время сессии;
    • данные теряются после перезагрузки.
  2. В derive-режиме (из пароля)

    • ключ генерируется через PBKDF2;
    • требуется ввод пароля при запуске.
  3. В IndexedDB без шифрования ключа

    • удобство выше;
    • безопасность ниже.
  4. Использование WebAuthn

    • аппаратная защита ключей;
    • высокая сложность реализации.

Работа с бинарными данными

localForage поддерживает Blob и ArrayBuffer, но при шифровании чаще используется единый формат:

  • ArrayBuffer → Uint8Array → ciphertext
  • обратное преобразование при чтении

Важно учитывать:

  • сериализация увеличивает размер данных;
  • base64-кодирование добавляет накладные расходы;
  • лучше хранить бинарные массивы напрямую, если драйвер поддерживает.

Производительность шифрования

Шифрование неизбежно влияет на скорость:

  • AES-GCM — наиболее быстрый вариант;
  • PBKDF2 может быть узким местом;
  • большие объёмы данных требуют потоковой обработки.

Типовые проблемы:

  • блокировка main thread при больших JSON-объектах;
  • задержки при частых setItem;
  • увеличение времени первой загрузки.

Оптимизации:

  • использование Web Workers;
  • батчинг операций записи;
  • кэширование уже зашифрованных объектов;
  • ленивое шифрование только критичных данных.

Версионирование схемы шифрования

При изменении алгоритма или структуры данных требуется поддержка миграций.

Добавление поля version позволяет различать форматы:

if (payload.version === 1) {
  return decryptV1(payload);
}
if (payload.version === 2) {
  return decryptV2(payload);
}

Это предотвращает потерю данных при обновлении приложения.


Типовые ошибки при реализации

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

  • повторное использование IV при AES-GCM;
  • хранение ключа рядом с зашифрованными данными;
  • отсутствие обработки ошибок расшифровки;
  • попытка шифровать уже сериализованные структуры без контроля типов;
  • игнорирование асинхронности localForage и Web Crypto.

Особенно критична ошибка повторного IV: она полностью снижает стойкость AES-GCM.


Комбинация с кэшированием

localForage часто используется как слой кэша. При добавлении шифрования возникает компромисс:

  • кэширование ускоряет доступ;
  • шифрование замедляет операции.

Рациональная стратегия:

  • шифровать только чувствительные данные;
  • не шифровать публичные кэши (UI-данные, справочники);
  • разделять хранилища по namespace.

Ограничения клиентского шифрования

Даже при корректной реализации остаются фундаментальные ограничения:

  • код приложения доступен пользователю;
  • ключ может быть извлечён при компрометации JS-контекста;
  • невозможно скрыть данные от администратора устройства;
  • защита не распространяется на runtime-атаку.

Клиентское шифрование следует рассматривать как дополнительный слой защиты, а не как полноценную систему безопасности.


Практическая модель безопасного хранилища

Сочетание подходов:

  • localForage как слой хранения;
  • Web Crypto API для шифрования;
  • строгая политика работы с ключами;
  • разделение данных по уровню чувствительности;
  • контроль версий схемы.

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