В стандартных сценариях JSON Web Token (JWT) предполагает одного получателя, который обладает ключом для проверки подписи или расшифровки содержимого. Однако спецификации семейства JOSE допускают более сложные конструкции, где один токен может быть предназначен сразу нескольким сторонам. Это достигается через механизм JSON Web Encryption (JWE) с множеством получателей.
В библиотеке jose для JavaScript подобный сценарий
реализуется через формат General JSON Serialization,
позволяющий включать массив получателей (recipients),
каждый из которых имеет собственные параметры шифрования ключа.
Для понимания множественных получателей важно различать форматы:
Пример структуры General JSON:
{
"protected": "base64url(...)",
"iv": "base64url(...)",
"ciphertext": "base64url(...)",
"tag": "base64url(...)",
"recipients": [
{
"header": { "alg": "RSA-OAEP" },
"encrypted_key": "base64url(...)"
},
{
"header": { "alg": "ECDH-ES" },
"encrypted_key": "base64url(...)"
}
]
}
Каждый получатель имеет собственный способ получения симметричного
ключа, используемого для расшифровки ciphertext.
Библиотека jose предоставляет класс
GeneralEncrypt для формирования такого токена.
npm install jose
import { GeneralEncrypt, generateKeyPair } from 'jose'
const { publicKey: rsaPublicKey } = await generateKeyPair('RSA-OAEP')
const { publicKey: ecPublicKey } = await generateKeyPair('ECDH-ES')
const encoder = new TextEncoder()
const payload = encoder.encode('Секретное сообщение')
const jwe = await new GeneralEncrypt(payload)
.setProtectedHeader({ enc: 'A256GCM' })
.addRecipient(rsaPublicKey, { alg: 'RSA-OAEP' })
.addRecipient(ecPublicKey, { alg: 'ECDH-ES' })
.encrypt()
Результат — объект в формате General JSON, содержащий зашифрованный
payload и несколько encrypted_key.
Генерируется случайный Content Encryption Key (CEK).
Payload шифруется с помощью CEK (например,
A256GCM).
Для каждого получателя:
encrypted_keyВсе encrypted_key включаются в массив
recipients.
Таким образом, каждый получатель может независимо расшифровать CEK и получить доступ к данным.
Получатель использует свой приватный ключ. Библиотека автоматически
находит соответствующий recipient.
import { generalDecrypt } from 'jose'
const { plaintext } = await generalDecrypt(jwe, privateKey)
const decoded = new TextDecoder().decode(plaintext)
Если ключ соответствует одному из получателей — расшифровка проходит успешно.
В реальных системах может быть несколько возможных ключей. В этом случае используется функция-резолвер:
const result = await generalDecrypt(jwe, async (protectedHeader, recipient) => {
if (recipient.header.alg === 'RSA-OAEP') {
return rsaPrivateKey
}
if (recipient.header.alg === 'ECDH-ES') {
return ecPrivateKey
}
})
Это позволяет динамически выбирать ключ в зависимости от алгоритма или других параметров.
В JWE используются три уровня заголовков:
enc)Пример:
.addRecipient(key, {
alg: 'RSA-OAEP',
kid: 'key-id-1'
})
kid помогает идентифицировать ключ при расшифровке.
JWT с несколькими получателями фактически является JWE, содержащим полезную нагрузку JWT.
const payload = JSON.stringify({
sub: '123',
role: 'admin'
})
После шифрования получается защищённый токен, который может быть прочитан разными сторонами.
1. Рассылка защищённых данных нескольким сервисам
Один токен может быть расшифрован:
2. Переход между ключами (key rotation)
Можно добавить:
Оба получателя смогут расшифровать токен в переходный период.
3. Мульти-tenant системы
Каждый клиент имеет свой ключ, но получает одинаковый защищённый payload.
Комбинации:
| alg | enc | Назначение |
|---|---|---|
| RSA-OAEP | A256GCM | универсальный вариант |
| ECDH-ES | A256GCM | высокая производительность |
encrypted_key независимПолезно декодировать JWE без расшифровки:
import { decodeProtectedHeader } from 'jose'
const header = decodeProtectedHeader(jwe)
console.log(header)
Анализ recipients:
console.log(jwe.recipients)
С увеличением числа получателей:
Оптимизация:
| Характеристика | JWS | JWE с несколькими получателями |
|---|---|---|
| Подпись | Да | Нет |
| Шифрование | Нет | Да |
| Несколько получателей | Нет | Да |
| Конфиденциальность | Нет | Да |
Часто применяется схема:
// JWS → JWE
Это обеспечивает:
При использовании JWE с A256GCM встроена аутентификация
данных через тег (tag). Это гарантирует, что:
aad (Additional Authenticated Data)Можно использовать набор ключей:
import { createLocalJWKSet } from 'jose'
const jwks = createLocalJWKSet({
keys: [/* массив ключей */]
})
await generalDecrypt(jwe, jwks)
Библиотека сама подберёт нужный ключ.
Типичные ошибки:
JWEDecryptionFailed — ключ не подошёлJWEInvalid — структура токена нарушенаJOSENotSupported — алгоритм не поддерживаетсяОбработка:
try {
await generalDecrypt(jwe, key)
} catch (e) {
console.error(e)
}
JWT с несколькими получателями — это:
Такая конструкция позволяет строить гибкие и безопасные системы обмена данными между множеством сторон, сохраняя единый источник истины и строгую криптографическую изоляцию ключей.