Расшифровка компактного JWE: compactDecrypt

Компактное представление JWE (JSON Web Encryption) — это строка, состоящая из пяти частей, разделённых точками:

  • protected header
  • encrypted key
  • initialization vector (IV)
  • ciphertext
  • authentication tag

Пример структуры:

xxxxx.yyyyy.zzzzz.aaaaa.bbbbb

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

Подготовка к расшифровке JWE

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

  • симметричный ключ (например, A256GCM)
  • асимметричный приватный ключ (например, RSA-OAEP, RSA-OAEP-256)
  • ключ в формате JWK или CryptoKey

Библиотека jose поддерживает работу с Web Crypto API и Node.js crypto модулем.

Основной метод: compactDecrypt

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

import { compactDecrypt } from 'jose'

Сигнатура:

compactDecrypt(jwe, key, options?)

Возвращает объект:

  • plaintext — расшифрованные данные (Uint8Array)
  • protectedHeader — заголовок JWE

Базовый пример расшифровки

import { compactDecrypt } from 'jose'

const jwe = 'eyJhbGciOi...'

const privateKey = {
  kty: 'RSA',
  n: '...',
  e: '...',
  d: '...',
  p: '...',
  q: '...',
  dp: '...',
  dq: '...',
  qi: '...'
}

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

const decoded = new TextDecoder().decode(plaintext)

console.log(protectedHeader)
console.log(decoded)

Работа с приватным RSA ключом

Чаще всего JWE создаётся с использованием RSA-OAEP, где шифруется симметрический ключ, а затем данным ключом шифруется payload.

При расшифровке:

  1. извлекается зашифрованный симметрический ключ
  2. он расшифровывается приватным RSA ключом
  3. полученный ключ используется для расшифровки payload

Этот процесс полностью инкапсулирован внутри compactDecrypt.

Использование CryptoKey (Node.js / WebCrypto)

В современных окружениях предпочтительно использовать CryptoKey.

import { compactDecrypt } from 'jose'
import { importPKCS8 } from 'jose/key/import'

const pkcs8 = `
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
`

const privateKey = await importPKCS8(pkcs8, 'RSA-OAEP-256')

const jwe = 'eyJhbGciOi...'

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

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

Разбор protected header

После расшифровки доступен заголовок JWE:

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

Типичный содержимое:

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

Значения:

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

Эти данные критичны для проверки корректности входного токена.

Валидация алгоритмов

При необходимости можно ограничить допустимые алгоритмы через параметры:

await compactDecrypt(jwe, privateKey, {
  keyManagementAlgorithms: ['RSA-OAEP-256'],
  contentEncryptionAlgorithms: ['A256GCM']
})

Это защищает от атак, связанных с подменой алгоритма.

Обработка ошибок

Расшифровка может завершиться ошибкой по нескольким причинам:

  • неверный ключ
  • повреждённый токен
  • несоответствие алгоритмов
  • некорректная структура JWE

Пример обработки:

try {
  const { plaintext } = await compactDecrypt(jwe, privateKey)
  console.log(new TextDecoder().decode(plaintext))
} catch (err) {
  console.error('Ошибка расшифровки JWE:', err)
}

Особенности работы с бинарными данными

plaintext возвращается в виде Uint8Array. Для преобразования в строку используется TextDecoder:

const decoded = new TextDecoder().decode(plaintext)

Если данные представляют JSON:

const json = JSON.parse(decoded)

Типичные алгоритмы в JWE

На практике встречаются следующие комбинации:

RSA + AES-GCM

  • alg: RSA-OAEP-256
  • enc: A256GCM

ECDH-ES

  • alg: ECDH-ES
  • enc: A256GCM

Direct symmetric encryption

  • alg: dir
  • enc: A128GCM, A256GCM

В случае dir никакого шифрования ключа нет — используется общий секрет.

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

import { compactDecrypt } from 'jose'

const secret = new TextEncoder().encode('super-secret-key-32bytes-long!!')

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

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

Важные нюансы безопасности

  • приватный ключ никогда не должен попадать в клиентский код
  • JWE не заменяет контроль доступа
  • проверка protectedHeader обязательна перед использованием данных
  • необходимо фиксировать допустимые алгоритмы

Поток обработки compactDecrypt

Логика внутри вызова можно представить так:

  1. декодирование compact-строки
  2. чтение protected header
  3. определение алгоритма
  4. расшифровка CEK (content encryption key)
  5. расшифровка ciphertext
  6. проверка authentication tag
  7. возврат plaintext

Все шаги выполняются внутри compactDecrypt, но понимание этой цепочки важно для отладки и архитектуры систем безопасности

Работа с большими payload

Расшифрованные данные не буферизуются как строки сразу — это бинарный поток. Это позволяет безопасно работать с большими сообщениями без лишнего копирования памяти.

Типовые ошибки при использовании compactDecrypt

  • использование неправильного ключа формата (например, JWK вместо CryptoKey без импорта)
  • несоответствие alg и типа ключа
  • попытка декодировать plaintext без TextDecoder
  • игнорирование проверки header

Интеграция в серверные приложения

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

  • API получает JWE
  • проверяет подпись/шифрование
  • расшифровывает payload
  • передаёт JSON в бизнес-логику

Это обеспечивает переносимость секретных данных без раскрытия содержимого в транспортном уровне