В спецификации JSON Web Encryption (JWE) предусмотрено несколько форм представления зашифрованного сообщения. Помимо компактной сериализации существует JSON-сериализация, которая в библиотеке jose реализуется через механизм General JWE. Такой формат используется, когда одно сообщение должно быть зашифровано для нескольких получателей с разными ключами, сохраняя единое тело шифротекста.
В отличие от компактного формата, где структура строго линейна и ориентирована на одного получателя, General JWE представляет собой объект с массивом recipients и отдельными уровнями заголовков. Это делает его более гибким, но и более сложным в построении.
В библиотеке jose работа с JSON-сериализацией шифрования реализована через класс GeneralEncrypt. Он позволяет формировать JWE-структуру, содержащую:
Каждый получатель использует свой механизм шифрования ключа контента (Content Encryption Key, CEK), но сам зашифрованный контент при этом общий.
Процесс формирования General JWE в jose включает несколько этапов:
Protected header содержит параметры, которые защищаются криптографически. Обычно сюда входят:
Этот заголовок одинаков для всех получателей.
Данные, которые не защищены криптографически и доступны всем участникам. Используется реже, но может содержать метаданные.
Каждый получатель может иметь собственный заголовок, содержащий параметры шифрования ключа или идентификатор ключа (kid).
Массив объектов, каждый из которых содержит:
Базовая структура использования в 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()
В этом примере создаётся один зашифрованный блок данных, доступный двум получателям с разными публичными ключами.
После выполнения 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 критично согласование алгоритмов:
Типичные комбинации:
Неправильное сочетание приводит к невозможности расшифровки.
При работе с несколькими ключами часто используется параметр kid для идентификации получателя:
.addRecipient(publicKey1, {
kid: 'service-a-key'
})
Это позволяет получателю быстро выбрать подходящий ключ без перебора.
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.
При вызове encrypt библиотека:
CEK остаётся одинаковым для всех recipients, что обеспечивает консистентность ciphertext.
Compact JWE:
General JWE:
На практике General JWE используется в системах, где:
При работе с GeneralEncrypt важны следующие аспекты:
Каждый recipient может получать индивидуальные параметры:
.addRecipient(publicKey1, {
kid: 'service-a',
crit: ['custom']
})
Это позволяет строить расширенные протоколы взаимодействия поверх JWE.
GeneralEncrypt реализует модель централизованного шифрования с последующим распределением ключей для множества получателей. Архитектура строится вокруг единого зашифрованного payload и набора индивидуально защищённых CEK, что обеспечивает баланс между производительностью и гибкостью криптографической системы.