Создание General JWE: GeneralEncrypt

В спецификации JSON Web Encryption (JWE) предусмотрено несколько форм представления зашифрованного сообщения. Помимо компактной сериализации существует JSON-сериализация, которая в библиотеке jose реализуется через механизм General JWE. Такой формат используется, когда одно сообщение должно быть зашифровано для нескольких получателей с разными ключами, сохраняя единое тело шифротекста.

В отличие от компактного формата, где структура строго линейна и ориентирована на одного получателя, General JWE представляет собой объект с массивом recipients и отдельными уровнями заголовков. Это делает его более гибким, но и более сложным в построении.

Модель General JWE в jose

В библиотеке jose работа с JSON-сериализацией шифрования реализована через класс GeneralEncrypt. Он позволяет формировать JWE-структуру, содержащую:

  • общий защищённый заголовок (protected header)
  • необязательный общий незашищённый заголовок (shared unprotected header)
  • список получателей (recipients), каждый из которых может иметь собственный заголовок и ключ

Каждый получатель использует свой механизм шифрования ключа контента (Content Encryption Key, CEK), но сам зашифрованный контент при этом общий.

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

Процесс формирования General JWE в jose включает несколько этапов:

  1. создание экземпляра GeneralEncrypt
  2. определение защищённого заголовка с алгоритмом
  3. добавление получателей с их публичными ключами
  4. завершение операции шифрования
  5. сериализация результата в JSON

Основные элементы структуры

Protected Header

Protected header содержит параметры, которые защищаются криптографически. Обычно сюда входят:

  • alg — алгоритм шифрования ключа (например, RSA-OAEP-256, ECDH-ES)
  • enc — алгоритм симметричного шифрования содержимого (например, A256GCM)

Этот заголовок одинаков для всех получателей.

Shared Unprotected Header

Данные, которые не защищены криптографически и доступны всем участникам. Используется реже, но может содержать метаданные.

Per-Recipient Header

Каждый получатель может иметь собственный заголовок, содержащий параметры шифрования ключа или идентификатор ключа (kid).

Recipients

Массив объектов, каждый из которых содержит:

  • header
  • encrypted_key

Создание General JWE через GeneralEncrypt

Базовая структура использования в Node.js:

import { GeneralEncrypt } from 'jose'
import { createPublicKey } from 'crypto'

const publicKey1 = createPublicKey(`-----BEGIN PUBLIC KEY-----...`)
const publicKey2 = createPublicKey(`-----BEGIN PUBLIC KEY-----...`)

const jwe = await new GeneralEncrypt(
  new TextEncoder().encode('секретные данные')
)
  .setProtectedHeader({
    alg: 'RSA-OAEP-256',
    enc: 'A256GCM'
  })
  .addRecipient(publicKey1)
  .addRecipient(publicKey2)
  .encrypt()

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

Структура результата General JWE

После выполнения encrypt() формируется JSON-объект следующего вида:

{
  "protected": "BASE64URL(...)",
  "recipients": [
    {
      "header": {
        "kid": "key-1"
      },
      "encrypted_key": "BASE64URL(...)"
    },
    {
      "header": {
        "kid": "key-2"
      },
      "encrypted_key": "BASE64URL(...)"
    }
  ],
  "iv": "BASE64URL(...)",
  "ciphertext": "BASE64URL(...)",
  "tag": "BASE64URL(...)"
}

Все получатели используют один и тот же ciphertext, но разные encrypted_key.

Работа с алгоритмами

При использовании GeneralEncrypt критично согласование алгоритмов:

  • alg определяет способ защиты CEK
  • enc определяет симметрическое шифрование данных

Типичные комбинации:

  • RSA-OAEP-256 + A256GCM
  • ECDH-ES + A256GCM
  • A256KW + A256GCM

Неправильное сочетание приводит к невозможности расшифровки.

Добавление идентификаторов ключей (kid)

При работе с несколькими ключами часто используется параметр kid для идентификации получателя:

.addRecipient(publicKey1, {
  kid: 'service-a-key'
})

Это позволяет получателю быстро выбрать подходящий ключ без перебора.

Использование JWK вместо PEM

Jose поддерживает работу с JWK (JSON Web Key), что упрощает переносимость ключей:

import { GeneralEncrypt, importJWK } from 'jose'

const jwk = {
  kty: 'RSA',
  e: 'AQAB',
  n: '...'
}

const publicKey = await importJWK(jwk, 'RSA-OAEP-256')

Далее ключ используется в addRecipient.

Механизм формирования CEK

При вызове encrypt библиотека:

  • генерирует случайный CEK
  • шифрует CEK для каждого получателя отдельно
  • использует CEK для шифрования payload через A256GCM или другой enc

CEK остаётся одинаковым для всех recipients, что обеспечивает консистентность ciphertext.

Различие между Compact и General JWE

Compact JWE:

  • один получатель
  • строковый формат
  • компактная структура

General JWE:

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

Сценарии применения GeneralEncrypt

На практике General JWE используется в системах, где:

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

Особенности безопасности

При работе с GeneralEncrypt важны следующие аспекты:

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

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

  • несоответствие alg и типа ключа (например, RSA ключ с ECDH-ES)
  • отсутствие kid при множестве ключей
  • попытка использовать приватный ключ вместо публичного
  • неправильная кодировка ключей при импорте

Расширенное использование с дополнительными заголовками

Каждый recipient может получать индивидуальные параметры:

.addRecipient(publicKey1, {
  kid: 'service-a',
  crit: ['custom']
})

Это позволяет строить расширенные протоколы взаимодействия поверх JWE.

Итоговая модель работы GeneralEncrypt

GeneralEncrypt реализует модель централизованного шифрования с последующим распределением ключей для множества получателей. Архитектура строится вокруг единого зашифрованного payload и набора индивидуально защищённых CEK, что обеспечивает баланс между производительностью и гибкостью криптографической системы.