Создание зашифрованного JWT: EncryptJWT

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

Внутри спецификации выделяются ключевые компоненты:

  • защищённый заголовок (Protected Header)
  • ключ управления контентом (CEK — Content Encryption Key)
  • алгоритм шифрования ключа
  • алгоритм шифрования содержимого
  • инициализационный вектор (IV)
  • зашифрованный текст (Ciphertext)
  • аутентификационный тег (Authentication Tag)

В jose эта модель скрыта за высокоуровневым API EncryptJWT.


Основные принципы работы EncryptJWT

EncryptJWT используется для создания JWE в формате Compact Serialization. Процесс включает последовательное формирование объекта JWT с последующим шифрованием полезной нагрузки.

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

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

Подготовка криптографического ключа

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

Симметричный ключ (Direct Encryption)

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

import { generateSecret } from 'jose'

const secret = await generateSecret('A256GCM')

Такой ключ подходит для сценариев внутри одной системы или доверенного окружения.


Асимметричный ключ (RSA / ECDH)

Для разделённых систем применяется публично-ключевая криптография.

import { generateKeyPair } from 'jose'

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

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


Создание зашифрованного JWT

Базовая структура формирования JWE через EncryptJWT строится следующим образом:

import { EncryptJWT } from 'jose'

Формирование payload

Payload представляет собой произвольный объект данных:

const payload = {
  sub: 'user_123',
  role: 'admin',
  permissions: ['read', 'write']
}

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

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

const jwt = await new EncryptJWT(payload)
  .setProtectedHeader({
    alg: 'dir',
    enc: 'A256GCM'
  })
  .setIssuedAt()
  .setExpirationTime('2h')
  .encrypt(secret)

Разбор этапов формирования

Protected Header

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

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

Пример:

{
  alg: 'dir',
  enc: 'A256GCM'
}

Стандартные claims

Методы setIssuedAt и setExpirationTime добавляют временные ограничения:

  • iat — время выпуска токена
  • exp — срок жизни

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

.setSubject('user_123')
.setIssuer('auth-service')
.setAudience('api')

Процесс шифрования

Вызов encrypt(secret) инициирует:

  • генерацию CEK (если требуется)
  • применение выбранного алгоритма шифрования
  • упаковку результата в Compact Serialization

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

header.encryptedKey.iv.ciphertext.tag

Алгоритмы шифрования

Симметричные схемы

  • dir + A128GCM
  • dir + A256GCM

Характеризуются минимальной задержкой и отсутствием обмена ключами.


Асимметричные схемы

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

Обеспечивают безопасную передачу ключа CEK.


Пример с RSA шифрованием

const jwt = await new EncryptJWT(payload)
  .setProtectedHeader({
    alg: 'RSA-OAEP-256',
    enc: 'A256GCM'
  })
  .setIssuedAt()
  .setExpirationTime('1h')
  .encrypt(publicKey)

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

import { jwtDecrypt } from 'jose'

const { payload: decrypted } = await jwtDecrypt(jwt, privateKey)

Работа с ECDH-ES

ECDH-ES применяется для эфемерного обмена ключами.

const jwt = await new EncryptJWT(payload)
  .setProtectedHeader({
    alg: 'ECDH-ES',
    enc: 'A256GCM'
  })
  .encrypt(publicKey)

Особенность заключается в генерации временного ключа для каждого сообщения.


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

Формат результата строго фиксирован:

BASE64URL(header).
BASE64URL(encryptedKey).
BASE64URL(iv).
BASE64URL(ciphertext).
BASE64URL(tag)

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


Обработка ошибок при шифровании

Частые причины сбоев:

  • несоответствие алгоритма ключу
  • использование неподходящего формата ключа (JWK vs CryptoKey)
  • отсутствие required claims
  • попытка шифрования неподдерживаемым алгоритмом

Пример типичной ошибки:

JWEInvalid: Invalid key or algorithm mismatch

Практические особенности использования EncryptJWT

Ограничение размера payload

JWE увеличивает размер данных из-за:

  • IV (инициализационного вектора)
  • тегов аутентификации
  • шифрованного ключа

Производительность

  • симметричное шифрование значительно быстрее RSA
  • ECDH-ES балансирует безопасность и скорость
  • повторное использование ключей снижает накладные расходы

Безопасное хранение ключей

Ключи должны храниться:

  • в переменных окружения (для симметричных сценариев)
  • в KMS (Key Management System) при асимметричной схеме
  • вне исходного кода

Совмещение EncryptJWT с другими механизмами jose

В реальных системах EncryptJWT часто используется совместно с:

  • jwtVerify — для подписанных токенов
  • jwtDecrypt — для извлечения payload
  • SignJWT — для гибридных схем (подпись + шифрование отдельно)

Гибридные сценарии применения

Типичный поток:

  1. формирование payload
  2. подпись через JWS
  3. шифрование через JWE

Такой подход обеспечивает:

  • проверку целостности
  • защиту содержимого
  • защиту от подмены данных на уровне транспорта

Особенности работы в Node.js и Edge-окружениях

Библиотека jose использует WebCrypto API, что обеспечивает:

  • совместимость с Node.js 16+
  • поддержку Edge Runtime (Cloudflare Workers, Vercel Edge)
  • отсутствие зависимости от OpenSSL-обёрток

Частые архитектурные ошибки

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

Поведение при декодировании

Дешифрование строго зависит от:

  • совпадения алгоритма
  • корректности ключа
  • целостности tag

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