JWE General Serialization используется в сценариях, где один зашифрованный объект должен быть доступен сразу нескольким получателям, при этом каждый получатель получает собственную зашифрованную копию ключа шифрования содержимого (CEK). В отличие от компактной сериализации, здесь структура JSON расширяется и позволяет описывать несколько recipients в одном JWE-документе.
General Serialization представляет собой JSON-структуру, в которой отдельно фиксируются:
Ключевой элемент — массив recipients, где каждый элемент содержит собственный зашифрованный ключ CEK для конкретного получателя.
Типичная структура General Serialization выглядит следующим образом:
{
"protected": "BASE64URL(...)",
"recipients": [
{
"header": {
"alg": "RSA-OAEP-256"
},
"encrypted_key": "BASE64URL(...)"
},
{
"header": {
"alg": "RSA-OAEP-256"
},
"encrypted_key": "BASE64URL(...)"
}
],
"iv": "BASE64URL(...)",
"ciphertext": "BASE64URL(...)",
"tag": "BASE64URL(...)"
}
Каждый recipient содержит:
header — параметры алгоритма для конкретного
получателяencrypted_key — CEK, зашифрованный публичным ключом
получателяВ библиотеке jose работа с General Serialization
реализована через API generalEncrypt.
Основная идея: создаётся единый зашифрованный payload, после чего к нему добавляются получатели с их публичными ключами.
Для примера используются RSA ключи в формате SPKI:
import { importSPKI } from 'jose'
const publicKeyA = await importSPKI(
`-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`,
'RSA-OAEP-256'
)
const publicKeyB = await importSPKI(
`-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`,
'RSA-OAEP-256'
)
Каждый получатель имеет собственную пару ключей, но в JWE используется только публичная часть.
Создание General JWE выполняется через
generalEncrypt:
import { generalEncrypt } from 'jose'
const encoder = new TextEncoder()
const payload = encoder.encode(
JSON.stringify({
sub: 'user-123',
role: 'admin',
permissions: ['read', 'write']
})
)
const jwe = await generalEncrypt(payload, encoder.encode('A256GCM'))
.setProtectedHeader({
enc: 'A256GCM'
})
.addRecipient(publicKeyA, {
alg: 'RSA-OAEP-256'
})
.addRecipient(publicKeyB, {
alg: 'RSA-OAEP-256'
})
.final()
Каждый вызов addRecipient выполняет несколько
операций:
encrypted_key для данного
recipientТаким образом, данные не дублируются, а ключи доступа разделяются.
Алгоритмически процесс выглядит так:
A256GCMКаждый получатель может восстановить данные только при наличии своего приватного ключа.
import { generalDecrypt } from 'jose'
const { plaintext } = await generalDecrypt(jwe, privateKeyA)
Если используется ключ не того получателя, расшифрование невозможно, так как CEK недоступен.
При наличии нескольких recipients:
В одном JWE возможно комбинировать разные алгоритмы для разных получателей:
.addRecipient(publicKeyA, { alg: 'RSA-OAEP-256' })
.addRecipient(ecPublicKeyB, { alg: 'ECDH-ES+A256KW' })
Это позволяет поддерживать гетерогенные криптографические системы.
Protected header хранится один раз и применяется ко всем recipients:
{
"enc": "A256GCM",
"typ": "JWE"
}
Этот заголовок подписывает параметры шифрования содержимого, но не влияет на способ доставки ключей.
General Serialization отличается от Compact тем, что:
Частые проблемы при использовании jose:
General JWE применяется в сценариях:
Структура позволяет масштабировать доступ без повторного шифрования payload, ограничиваясь только операциями с CEK.