Ограничения экспорта и флаг extractable

Модель управляемых криптографических ключей

Web Crypto API проектирует работу с ключами так, чтобы минимизировать возможность их утечки из безопасного контекста. В отличие от «сырых» криптографических библиотек, где ключи часто представлены в виде обычных байтовых массивов, в браузере ключи существуют как объект CryptoKey, управляемый рантаймом.

Каждый ключ в Web Crypto API характеризуется набором свойств:

  • алгоритм (RSA, ECDSA, AES и т.д.)
  • назначение (keyUsages)
  • возможность извлечения (extractable)
  • тип хранения (в памяти браузера или в защищённом контексте)

Ключевым элементом этой модели является флаг extractable, который определяет, можно ли получить «сырой» вид ключа через экспорт.


Что означает extractable

Флаг extractable задаётся при создании или импорте ключа:

  • через crypto.subtle.generateKey
  • через crypto.subtle.importKey

Он принимает булево значение:

  • true — ключ можно экспортировать
  • false — ключ нельзя экспортировать никаким способом

Пример генерации:

const key = await crypto.subtle.generateKey(
  {
    name: "AES-GCM",
    length: 256
  },
  false, // extractable
  ["encrypt", "decrypt"]
);

Если значение установлено в false, браузер гарантирует, что ключ нельзя будет получить в виде массива байтов или JSON-представления.


Экспорт ключей и его форматы

Web Crypto API поддерживает экспорт через:

  • crypto.subtle.exportKey
  • crypto.subtle.wrapKey

Поддерживаемые форматы зависят от типа ключа:

Симметричные ключи (AES)

  • "raw" — байтовый массив (ArrayBuffer)
  • "jwk" — JSON Web Key

Ассиметричные ключи

  • "spki" — публичный ключ
  • "pkcs8" — приватный ключ
  • "jwk" — JSON Web Key

Пример экспорта:

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

Поведение при extractable = false

Если ключ создан с параметром:

extractable: false

то любая попытка экспорта приводит к ошибке:

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

await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
// InvalidAccessError

Важно:

  • ошибка возникает независимо от формата экспорта
  • даже jwk становится недоступным
  • ограничение проверяется браузером, а не алгоритмом

Семантика безопасности extractable

Флаг extractable является одним из ключевых механизмов защиты:

1. Защита от утечки ключа через JavaScript

Даже если злоумышленник получил доступ к выполнению скрипта на странице (XSS), он не сможет извлечь ключ, если он неэкстрагируемый.

2. Разделение «используемых» и «переносимых» ключей

  • extractable = true ключ можно:

    • экспортировать
    • сохранять
    • передавать
  • extractable = false ключ можно только:

    • использовать внутри crypto.subtle
    • передавать между операциями API

3. Защита приватных ключей

Наиболее критичный сценарий — приватные ключи RSA/ECDSA:

crypto.subtle.generateKey(
  {
    name: "ECDSA",
    namedCurve: "P-256"
  },
  false,
  ["sign"]
);

В этом случае приватный ключ нельзя извлечь даже в виде pkcs8.


Ограничение: extractable нельзя изменить

После создания ключа значение extractable становится неизменяемым.

Невозможно:

  • «сделать ключ экспортируемым»
  • «снять ограничение»
  • «переконвертировать ключ»

Единственный способ получить другой режим — создать новый ключ.


Влияние на импорт ключей

При импорте ключа флаг также фиксируется:

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

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

extractable: false

Это позволяет «заблокировать» ключ сразу после загрузки.


Ограничения экспортируемости и форматы

Некоторые ограничения связаны не только с флагом, но и с алгоритмом:

1. Не все алгоритмы поддерживают экспорт

Например:

  • ключи, созданные через некоторые аппаратные реализации (если доступны)
  • ключи, привязанные к контексту исполнения

2. Формат зависит от типа ключа

Тип ключа Возможный экспорт
AES raw, jwk
RSA public spki, jwk
RSA private pkcs8, jwk
ECDSA spki/pkcs8, jwk

WrapKey как обходной путь (но не обход ограничения)

Даже если ключ неэкстрагируемый, он может быть обёрнут:

const wrapped = await crypto.subtle.wrapKey(
  "raw",
  key,
  wrappingKey,
  "AES-GCM"
);

Но важно:

  • wrapKey не раскрывает сам ключ
  • он лишь шифрует его для переноса
  • если ключ неэкстрагируемый, некоторые сценарии wrap также могут быть недоступны в зависимости от реализации браузера и типа ключа

Хранение ключей и extractable

Часто ключи сохраняются через IndexedDB:

  • extractable = true → можно экспортировать и сохранить вручную
  • extractable = false → сохраняется сам CryptoKey объект

Пример:

const db = indexedDB.open("keys", 1);

// CryptoKey сохраняется напрямую
store.put(key, "my-key");

Браузер сериализует ключ в защищённом формате, недоступном для JavaScript.


Практические ограничения при разработке

1. Невозможность бэкапа ключа

Если ключ создан как non-extractable:

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

2. Зависимость от хранилища браузера

Такие ключи фактически привязаны к:

  • профилю браузера
  • устройству
  • иногда — к origin

3. Потеря ключа = невозможность расшифровки

Если ключ удалён или профиль повреждён:

  • данные, зашифрованные этим ключом, становятся недоступны

Типичные сценарии использования extractable = false

Подпись данных

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

Приватный ключ используется только внутри браузера.


Шифрование пользовательских данных

AES ключи для локального шифрования:

await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

Ключевая особенность архитектуры Web Crypto

Флаг extractable — это не просто ограничение API, а часть модели доверия:

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

Это приближает Web Crypto API к аппаратным модулям безопасности (HSM-подобная модель), но на уровне программной среды браузера.