Компактная сериализация JWE

Компактная сериализация JWE — это самый распространённый формат представления зашифрованного JSON Web Encryption, в котором все компоненты токена упакованы в одну строку, разделённую точками. Именно этот формат используется в большинстве веб-приложений, API и OAuth/OIDC сценариях благодаря своей компактности и удобству передачи через HTTP-заголовки и URL-параметры.

В библиотеке jose работа с JWE компактной сериализацией строится вокруг высокоуровневых функций compactEncrypt и compactDecrypt, которые инкапсулируют всю криптографическую сложность.

Формат JWE Compact Serialization всегда состоит из пяти частей:

BASE64URL(Protected Header) . 
BASE64URL(Encrypted Key) . 
BASE64URL(Initialization Vector) . 
BASE64URL(Ciphertext) . 
BASE64URL(Authentication Tag)

Каждая часть имеет строго определённое назначение:

  • Protected Header — содержит метаданные о алгоритмах шифрования
  • Encrypted Key — зашифрованный CEK (Content Encryption Key)
  • Initialization Vector (IV) — случайный вектор инициализации для AES-GCM
  • Ciphertext — зашифрованные данные полезной нагрузки
  • Authentication Tag — обеспечивает целостность и подлинность данных

Эта структура фиксирована и не может быть изменена без нарушения спецификации.

Алгоритмическая модель JWE

JWE использует двухуровневую схему:

  1. Key Encryption Algorithm (alg) — защищает симметричный ключ (CEK)
  2. Content Encryption Algorithm (enc) — шифрует сам payload

Пример распространённых комбинаций:

  • RSA-OAEP-256 + A256GCM
  • ECDH-ES + A256GCM
  • dir + A256GCM (прямое использование ключа)

Создание JWE Compact Serialization в jose

В библиотеке jose процесс шифрования выглядит следующим образом:

import { compactEncrypt } from 'jose'

const encoder = new TextEncoder()

const jwe = await new compactEncrypt(
  encoder.encode('Секретное сообщение')
)
  .setProtectedHeader({
    alg: 'RSA-OAEP-256',
    enc: 'A256GCM'
  })
  .encrypt(publicKey)

В этом примере:

  • alg определяет алгоритм шифрования ключа
  • enc задаёт алгоритм шифрования содержимого
  • publicKey используется для шифрования CEK

Результатом будет строка вида:

eyJhbGciOiJS... . 
OKOawDo13gRp2... . 
48V1_ALb6US04U3e... . 
5eym8Tw6gH7s... . 
XFBoMYUZodetZdvTiFvSkQ

Декодирование JWE Compact Serialization

Расшифровка выполняется симметричной операцией:

import { compactDecrypt } from 'jose'

const { plaintext } = await compactDecrypt(jwe, privateKey)

console.log(new TextDecoder().decode(plaintext))

На этом этапе:

  • извлекается Encrypted Key
  • расшифровывается CEK с использованием приватного ключа
  • затем расшифровывается ciphertext через AES-GCM

Роль Protected Header

Protected Header всегда включается в процесс аутентификации. Он кодируется в base64url и участвует в вычислении authentication tag.

Пример header:

{
  "alg": "RSA-OAEP-256",
  "enc": "A256GCM",
  "typ": "JWE"
}

Изменение любого поля header после шифрования приводит к невозможности верификации тегов.

Initialization Vector и безопасность

IV (Initialization Vector) в JWE:

  • всегда уникален для каждой операции шифрования
  • обычно имеет длину 96 бит для AES-GCM
  • генерируется случайным образом

Повтор IV с тем же ключом приводит к критическим уязвимостям в режиме GCM.

Content Encryption Key (CEK)

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

Процесс:

  1. генерируется случайный CEK
  2. CEK шифруется публичным ключом получателя (или согласуется через ECDH)
  3. зашифрованный CEK помещается во вторую часть JWE

Таким образом достигается гибридная криптография:

  • асимметричная защита ключа
  • симметричная эффективность шифрования данных

Отличия compact от JSON сериализации

Compact Serialization имеет ряд ограничений:

  • только один получатель (recipient)
  • отсутствует структура JSON с массивами recipients
  • нельзя включать дополнительные метаданные
  • оптимизирован для передачи в URL и HTTP заголовках

В отличие от него JSON Serialization поддерживает:

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

Типовые ошибки при работе с JWE compact

В реальных системах часто встречаются следующие проблемы:

  • несоответствие alg и типа ключа (например, RSA-OAEP-256 с симметричным ключом)
  • повторное использование IV
  • попытка декодировать JWE без приватного ключа
  • несоответствие enc алгоритма между encrypt/decrypt сторонами

Использование прямого симметричного режима (dir)

При использовании alg: "dir" CEK не шифруется, а используется напрямую:

const jwe = await new compactEncrypt(
  encoder.encode('data')
)
  .setProtectedHeader({
    alg: 'dir',
    enc: 'A256GCM'
  })
  .encrypt(sharedSecretKey)

Это упрощает структуру, но требует безопасного обмена ключом заранее.

Внутренние этапы формирования токена

При вызове encrypt() выполняется последовательность:

  1. Генерация CEK
  2. Шифрование plaintext через AES-GCM
  3. Шифрование CEK (или его прямое использование)
  4. Формирование Protected Header
  5. Кодирование всех частей в base64url
  6. Конкатенация через точки

Каждый этап строго детерминирован спецификацией JWE (RFC 7516), что обеспечивает совместимость между различными реализациями.

Производительность compact формата

Compact Serialization оптимизирована под:

  • минимальный размер строки
  • отсутствие JSON-оверхаеда
  • быстрый парсинг
  • использование в JWT/JWE pipeline

Поэтому именно этот формат используется в большинстве реализаций OAuth2/OIDC и систем авторизации на базе JWT + JWE.