JWE с несколькими получателями в General-сериализации

JWE General Serialization используется в сценариях, где один зашифрованный объект должен быть доступен сразу нескольким получателям, при этом каждый получатель получает собственную зашифрованную копию ключа шифрования содержимого (CEK). В отличие от компактной сериализации, здесь структура JSON расширяется и позволяет описывать несколько recipients в одном JWE-документе.

General Serialization представляет собой JSON-структуру, в которой отдельно фиксируются:

  • защищённый заголовок (protected header)
  • общие параметры шифрования
  • список получателей (recipients)
  • инициализационный вектор (iv)
  • зашифрованные данные (ciphertext)
  • аутентификационный тег (tag)

Ключевой элемент — массив recipients, где каждый элемент содержит собственный зашифрованный ключ CEK для конкретного получателя.

Структура JWE с несколькими получателями

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

{
  "protected": "BASE64URL(...)",
  "recipients": [
    {
      "header": {
        "alg": "RSA-OAEP-256"
      },
      "encrypted_key": "BASE64URL(...)"
    },
    {
      "header": {
        "alg": "RSA-OAEP-256"
      },
      "encrypted_key": "BASE64URL(...)"
    }
  ],
  "iv": "BASE64URL(...)",
  "ciphertext": "BASE64URL(...)",
  "tag": "BASE64URL(...)"
}

Каждый recipient содержит:

  • header — параметры алгоритма для конкретного получателя
  • encrypted_key — CEK, зашифрованный публичным ключом получателя

Подход библиотеки jose

В библиотеке jose работа с General Serialization реализована через API generalEncrypt.

Основная идея: создаётся единый зашифрованный payload, после чего к нему добавляются получатели с их публичными ключами.

Подготовка ключей

Для примера используются RSA ключи в формате SPKI:

import { importSPKI } from 'jose'

const publicKeyA = await importSPKI(
  `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`,
  'RSA-OAEP-256'
)

const publicKeyB = await importSPKI(
  `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`,
  'RSA-OAEP-256'
)

Каждый получатель имеет собственную пару ключей, но в JWE используется только публичная часть.

Формирование JWE с несколькими получателями

Создание General JWE выполняется через generalEncrypt:

import { generalEncrypt } from 'jose'

const encoder = new TextEncoder()

const payload = encoder.encode(
  JSON.stringify({
    sub: 'user-123',
    role: 'admin',
    permissions: ['read', 'write']
  })
)

const jwe = await generalEncrypt(payload, encoder.encode('A256GCM'))
  .setProtectedHeader({
    enc: 'A256GCM'
  })
  .addRecipient(publicKeyA, {
    alg: 'RSA-OAEP-256'
  })
  .addRecipient(publicKeyB, {
    alg: 'RSA-OAEP-256'
  })
  .final()

Механика работы addRecipient

Каждый вызов addRecipient выполняет несколько операций:

  • генерируется единый Content Encryption Key (CEK)
  • CEK шифруется публичным ключом конкретного получателя
  • результат помещается в encrypted_key для данного recipient
  • общий ciphertext остаётся единым для всех получателей

Таким образом, данные не дублируются, а ключи доступа разделяются.

Общий процесс шифрования

Алгоритмически процесс выглядит так:

  1. Генерируется CEK (симметричный ключ)
  2. Генерируется IV (initialization vector)
  3. Payload шифруется алгоритмом A256GCM
  4. CEK шифруется публичным ключом каждого получателя отдельно
  5. Формируется JSON структура General Serialization

Расшифрование для конкретного получателя

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

import { generalDecrypt } from 'jose'

const { plaintext } = await generalDecrypt(jwe, privateKeyA)

Если используется ключ не того получателя, расшифрование невозможно, так как CEK недоступен.

Поведение при множественных получателях

При наличии нескольких recipients:

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

Различие алгоритмов для recipients

В одном JWE возможно комбинировать разные алгоритмы для разных получателей:

.addRecipient(publicKeyA, { alg: 'RSA-OAEP-256' })
.addRecipient(ecPublicKeyB, { alg: 'ECDH-ES+A256KW' })

Это позволяет поддерживать гетерогенные криптографические системы.

Практическое устройство protected header

Protected header хранится один раз и применяется ко всем recipients:

{
  "enc": "A256GCM",
  "typ": "JWE"
}

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

Особенности сериализации

General Serialization отличается от Compact тем, что:

  • Compact поддерживает только одного получателя
  • General допускает массив recipients
  • формат строго JSON-структурированный
  • подходит для распределённых систем и группового шифрования

Типичные ошибки при работе с multiple recipients

Частые проблемы при использовании jose:

  • использование одного и того же publicKey для разных алгоритмов без указания alg
  • попытка дешифровать чужим приватным ключом
  • смешение несовместимых алгоритмов в одном JWE
  • неправильная сериализация результата (ожидание compact вместо general)

Использование в распределённых системах

General JWE применяется в сценариях:

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

Структура позволяет масштабировать доступ без повторного шифрования payload, ограничиваясь только операциями с CEK.