JSON-сериализация JWE: Flattened и General

Общая модель JWE и роль JSON-сериализации

JWE (JSON Web Encryption) описывает формат защищённого сообщения, в котором данные шифруются и, при необходимости, дополнительно аутентифицируются. В экосистеме JavaScript для работы с JWE широко используется библиотека Jose, реализующая спецификации JOSE (JSON Object Signing and Encryption).

В отличие от компактной сериализации, JSON-сериализация JWE предназначена для сценариев, где требуется более гибкая структура: работа с несколькими получателями, раздельное представление ключевых материалов и расширяемость метаданных.

JSON-сериализация JWE существует в двух вариантах:

  • Flattened JSON Serialization
  • General JSON Serialization

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


Структура JWE: базовые компоненты

Любая JWE-структура включает следующие элементы:

  • protected — защищённый заголовок (base64url)
  • unprotected — незашифрованный заголовок (опционально)
  • iv — вектор инициализации
  • ciphertext — зашифрованные данные
  • tag — аутентификационный тег
  • encrypted_key — зашифрованный CEK (Content Encryption Key)

Дополнительно могут использоваться:

  • aad (Additional Authenticated Data)
  • индивидуальные заголовки получателей

Flattened JSON Serialization

Flattened-формат применяется, когда у JWE ровно один получатель. Это упрощённое представление, в котором данные о получателе находятся на верхнем уровне объекта.

Структура Flattened JWE

{
  "protected": "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIn0",
  "encrypted_key": "gW7a9...example...",
  "iv": "48V1_ALb6US04U3b",
  "ciphertext": "5eym8...example...",
  "tag": "XFBoMYUZodetZdvTiFvSkQ"
}

Особенности Flattened формата

  • Поддерживает только одного получателя
  • Минимальная избыточность
  • Удобен для API с точкой-точка (client-server)
  • Часто используется в мобильных и backend-сервисах

Использование в Jose

Пример шифрования через Jose:

import { JWE, generateKeyPair } from 'jose'

const { publicKey } = await generateKeyPair('RSA-OAEP-256')

const encoder = new TextEncoder()

const jwe = await new JWE.Encrypt(
  encoder.encode('секретные данные')
)
  .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
  .encrypt(publicKey)

console.log(jwe)

Результат будет в формате Flattened JSON Serialization.


General JSON Serialization

General JSON Serialization используется при необходимости шифрования для нескольких получателей. Это расширенный формат, в котором каждый получатель описывается отдельно.

Структура General JWE

{
  "protected": "eyJlbmMiOiJBMTI4R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0",
  "recipients": [
    {
      "encrypted_key": "abc123...recipient1..."
    },
    {
      "encrypted_key": "def456...recipient2..."
    }
  ],
  "iv": "48V1_ALb6US04U3b",
  "ciphertext": "5eym8...example...",
  "tag": "XFBoMYUZodetZdvTiFvSkQ"
}

Особенности General формата

  • Поддерживает несколько получателей
  • Каждый recipient имеет собственный encrypted_key
  • Общие данные шифруются один раз (ciphertext общий)
  • Позволяет реализовать групповую доставку секретов

Сравнение Flattened и General форматов

Количество получателей

  • Flattened: строго 1
  • General: 1 и более

Структура данных

Flattened:

  • encrypted_key на верхнем уровне

General:

  • массив recipients

Сценарии использования

Flattened:

  • REST API
  • мобильные приложения
  • однонаправленная передача данных

General:

  • корпоративные системы
  • распределённые сервисы
  • multi-tenant архитектуры
  • групповые сообщения

Работа с несколькими получателями

В General JSON Serialization каждый получатель получает свой вариант CEK, зашифрованный его публичным ключом.

Процесс:

  1. Генерируется CEK (Content Encryption Key)
  2. CEK шифруется для каждого получателя отдельно
  3. Формируется массив recipients
  4. Основные данные шифруются один раз с использованием CEK

Пример: шифрование для двух получателей

import { JWE, generateKeyPair } from 'jose'

const encoder = new TextEncoder()

const { publicKey: publicKey1 } = await generateKeyPair('RSA-OAEP-256')
const { publicKey: publicKey2 } = await generateKeyPair('RSA-OAEP-256')

const jwe = await new JWE.Encrypt(
  encoder.encode('общие секретные данные')
)
  .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
  .addRecipient(publicKey1)
  .addRecipient(publicKey2)
  .encrypt()

console.log(JSON.stringify(jwe, null, 2))

Результат автоматически будет представлен в General JSON Serialization.


Поля protected и unprotected

Protected Header

Содержит параметры, влияющие на криптографическую обработку:

  • alg — алгоритм шифрования ключа
  • enc — алгоритм шифрования данных
  • typ — тип токена

Этот заголовок всегда включается в вычисление аутентификационного тега.


Unprotected Header

Используется для метаданных, которые не участвуют в криптографической защите:

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

AAD (Additional Authenticated Data)

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

Пример:

jwe.setAdditionalAuthenticatedData(
  encoder.encode('metadata')
)

Эти данные не попадают в ciphertext, но защищаются тегом.


Внутренний процесс формирования JWE

  1. Генерация CEK
  2. Шифрование CEK для каждого получателя
  3. Шифрование полезной нагрузки
  4. Вычисление authentication tag
  5. Формирование структуры JSON

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

Библиотека Jose автоматически:

  • определяет формат сериализации
  • управляет CEK
  • корректно формирует recipients
  • поддерживает стандарты JWE (RFC 7516)

Типичные ошибки при работе с JSON-сериализацией

Использование Flattened для нескольких получателей

Flattened формат не поддерживает массив recipients. Попытка добавить второго получателя приводит к ошибке или невалидному JWE.


Несоответствие alg и enc

Неверная комбинация:

  • RSA-OAEP + A128GCM несовместимые настройки в некоторых конфигурациях

Потеря protected header

Любое изменение protected после шифрования делает токен недействительным.


Практические рекомендации

  • Flattened формат использовать по умолчанию при одном получателе
  • General формат использовать только при реальной необходимости мультиадресации
  • Всегда фиксировать alg и enc явно
  • Избегать смешивания unprotected и protected логики без необходимости
  • Контролировать размер recipients в General формате для производительности