Хранение ключей в IndexedDB

Хранение криптографических ключей в Web Crypto API тесно связано с особенностями объекта CryptoKey и механизмом сериализации, который используется в браузере. В отличие от обычных JavaScript-объектов, ключи не могут быть произвольно преобразованы в JSON или сохранены через localStorage без потери структуры и безопасности. Именно IndexedDB становится стандартным способом долговременного хранения ключевого материала на стороне клиента.

CryptoKey представляет собой непрозрачную ссылку на ключ, управляемый браузером. Он не содержит «сырых» байтов ключа в доступном виде и не подлежит прямой сериализации через JSON.stringify.

Ключевые свойства CryptoKey:

  • type: public, private или secret
  • extractable: определяет возможность экспорта ключа
  • algorithm: описание криптографического алгоритма
  • usages: допустимые операции (например, encrypt, decrypt, sign, verify)

Важно учитывать, что даже при наличии объекта в памяти его содержимое может быть недоступно для извлечения, если extractable установлено в false.

Поддержка CryptoKey в IndexedDB

IndexedDB использует structured clone algorithm, который поддерживает хранение CryptoKey без необходимости ручной сериализации. Это делает его единственным штатным механизмом браузера для долговременного хранения ключей Web Crypto API.

Поддерживаемые сущности:

  • CryptoKey
  • ArrayBuffer
  • TypedArray
  • сложные объекты (при соблюдении правил clone)

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

Создание и сохранение ключа

Пример генерации симметричного ключа AES и его сохранения:

const dbName = "crypto-db";
const storeName = "keys";

function openDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(dbName, 1);

    request.onupgradenee ded = () => {
      const db = request.result;
      db.createObjectStore(storeName);
    };

    request.onsucc ess = () => resolve(request.result);
    request.oner ror = () => reject(request.error);
  });
}

async function saveKey(name, key) {
  const db = await openDB();

  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, "readwrite");
    const store = tx.objectStore(storeName);

    store.put(key, name);

    tx.oncompl ete = () => resolve(true);
    tx.oner ror = () => reject(tx.error);
  });
}

async function generateAndStoreKey() {
  const key = await crypto.subtle.generateKey(
    {
      name: "AES-GCM",
      length: 256
    },
    true,
    ["encrypt", "decrypt"]
  );

  await saveKey("aes-key-1", key);
}

В этом примере ключ сохраняется напрямую как значение IndexedDB без дополнительной сериализации.

Получение ключа из IndexedDB

Извлечение ключа происходит так же, как и чтение любого объекта:

async function getKey(name) {
  const db = await openDB();

  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, "readonly");
    const store = tx.objectStore(storeName);
    const request = store.get(name);

    request.onsucc ess = () => resolve(request.result);
    request.oner ror = () => reject(request.error);
  });
}

async function useKey() {
  const key = await getKey("aes-key-1");

  const data = new TextEncoder().encode("secret message");

  const iv = crypto.getRandomValues(new Uint8Array(12));

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

  return encrypted;
}

Ключ восстанавливается в виде полноценного CryptoKey, готового к использованию в операциях Web Crypto API.

Ограничения extractable и стратегия хранения

Свойство extractable определяет возможность экспорта ключа через crypto.subtle.exportKey.

  • extractable: true — ключ можно экспортировать (например, в JWK, PKCS#8)
  • extractable: false — ключ остаётся исключительно внутри Web Crypto

При хранении в IndexedDB это свойство сохраняется, но не влияет на сам факт сохранения. Однако оно критично для безопасности: даже при наличии доступа к IndexedDB экспорт невозможен, если ключ не extractable.

Типичная стратегия:

  • симметричные ключи AES: часто extractable: false
  • публичные ключи: могут быть extractable: true
  • приватные ключи: предпочтительно extractable: false

Хранение асимметричных ключевых пар

Генерация RSA-пары:

const keyPair = await crypto.subtle.generateKey(
  {
    name: "RSA-PSS",
    modulusLength: 2048,
    publicExponent: new Uint8Array([1, 0, 1]),
    hash: "SHA-256"
  },
  true,
  ["sign", "verify"]
);

await saveKey("rsa-private", keyPair.privateKey);
await saveKey("rsa-public", keyPair.publicKey);

При необходимости можно хранить ключи раздельно, используя разные записи IndexedDB.

Версионирование ключей

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

  • добавление суффикса версии в ключ (key-v1, key-v2)
  • хранение метаданных рядом с CryptoKey
  • отдельные objectStore для разных поколений ключей

Пример структуры:

  • keys_v1
  • keys_v2
  • или единый store с объектом { key, version, createdAt }

Метаданные ключей

Часто требуется хранить не только сам CryptoKey, но и сопутствующую информацию:

const record = {
  key,
  algorithm: "AES-GCM",
  createdAt: Date.now(),
  usages: ["encrypt", "decrypt"]
};

IndexedDB позволяет хранить такие структуры без ограничений, что делает возможным построение полноценного хранилища ключевого менеджера на клиенте.

Экспорт и импорт ключей для переноса

При необходимости переноса ключа между устройствами используется экспорт:

const raw = await crypto.subtle.exportKey("jwk", key);

Импорт:

const importedKey = await crypto.subtle.importKey(
  "jwk",
  raw,
  { name: "AES-GCM" },
  true,
  ["encrypt", "decrypt"]
);

После импорта ключ снова может быть сохранён в IndexedDB как обычный CryptoKey.

Безопасность хранения в IndexedDB

IndexedDB изолирован по origin, что означает:

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

Однако существуют важные ограничения:

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

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

Частые ошибки при работе с CryptoKey и IndexedDB

  • попытка сериализовать CryptoKey через JSON — приводит к потере данных
  • использование localStorage вместо IndexedDB — невозможность хранения объектов
  • ожидание доступности ключа вне контекста origin
  • попытка экспортировать ключ с extractable: false
  • хранение ключей без учета версии алгоритма

Асинхронная природа операций

Работа с IndexedDB и Web Crypto полностью асинхронна. Это влияет на архитектуру:

  • невозможность синхронного доступа к ключам
  • необходимость построения цепочек async/await
  • задержка при первом доступе (cold start базы)

В результате часто используется инициализация уровня приложения:

let cachedKey;

async function init() {
  cachedKey = await getKey("aes-key-1");
}

Использование транзакций при работе с ключами

IndexedDB требует использования транзакций для каждой операции:

  • readonly для чтения ключей
  • readwrite для записи и обновления

Каждая транзакция атомарна, что гарантирует целостность ключевого хранилища при конкурентном доступе.

Практика разделения хранилища

Для сложных приложений часто выделяются отдельные objectStore:

  • crypto-keys — для CryptoKey
  • key-metadata — для описаний
  • key-index — для быстрых поисков

Такое разделение снижает нагрузку и упрощает миграции структуры данных.

Ограничения платформы

Несмотря на поддержку хранения CryptoKey, существуют ограничения:

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

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