gose/jose/jwe

JSON Web Encryption (JWE) - RFC 7516

Encryption using algorithms from RFC 7518: key encryption (RSA-OAEP, ECDH-ES, AES Key Wrap, AES-GCM Key Wrap, PBES2, dir) and content encryption (AES-GCM, AES-CBC-HMAC).

Non-standard extensions are also supported: ChaCha20 Key Wrap (C20PKW, XC20PKW), ECDH-ES+ChaCha20KW, and ChaCha20-Poly1305/XChaCha20-Poly1305 content encryption.

Example

import gose/jose/jwe
import gose/algorithm
import gose/key

let key = key.generate_enc_key(algorithm.AesGcm(algorithm.Aes256))
let plaintext = <<"hello world":utf8>>

// Create and encrypt a JWE using direct encryption
let assert Ok(encrypted) = jwe.new_direct(algorithm.AesGcm(algorithm.Aes256))
  |> jwe.encrypt(key, plaintext)

// Serialize to compact format
let assert Ok(token) = jwe.serialize_compact(encrypted)

// Parse and decrypt with algorithm pinning
let assert Ok(parsed) = jwe.parse_compact(token)
let assert Ok(decryptor) = jwe.key_decryptor(algorithm.Direct, algorithm.AesGcm(algorithm.Aes256), [key])
let assert Ok(decrypted) = jwe.decrypt(decryptor, parsed)

Phantom Types

Jwe(state, family, origin) carries three phantom parameters. The state is Unencrypted before encrypt and Encrypted after, gating serialization and decryption on a completed encryption. The family is one of Direct, AesKw, AesGcmKw, Rsa, EcdhEs, or Pbes2; it restricts algorithm-specific builders so with_apu/with_apv only compile on EcdhEs JWEs and with_p2c only compiles on Pbes2 JWEs. The origin is Built for values produced by a new_* builder and encrypt, and Parsed for values from parse_compact or parse_json.

Algorithm Pinning

Algorithm pinning prevents algorithm confusion attacks:

  1. JWK alg metadata: If a key has alg set via key.with_alg, the JWE algorithm must match during encryption and decryption.
  2. Decryptor API: jwe.decrypt() with a Decryptor pins both key encryption and content encryption algorithms; mismatches are rejected.
  3. Key type validation: The key type must match the algorithm (RSA for RSA-OAEP, EC for ECDH-ES, etc.).

For strongest security, always set the alg field on keys or use decryptors.

Unprotected Headers

JWE supports unprotected headers at two levels in JSON serialization. The unprotected field carries shared headers that apply to all recipients, and each recipient’s header field carries headers specific to that recipient.

Security Warning: Unprotected headers are NOT integrity protected. They can be modified by an attacker without detection. Security-critical parameters (alg, enc, crit, zip) are rejected and must be integrity protected.

Use with_shared_unprotected and with_unprotected to add headers during creation. Use decode_shared_unprotected_header and decode_unprotected_header to read parsed headers.

Critical Header Support

The crit header is validated per RFC 7516:

Key Metadata

JWK metadata (use, key_ops) is enforced during encryption and decryption. Keys with incompatible metadata are rejected.

Compression Not Supported

The zip header (DEFLATE compression) is intentionally not supported. Compression before encryption leaks information about plaintext through ciphertext size variations (CRIME/BREACH-style attacks). JWEs with zip set are rejected during parsing.

JSON Serialization Limitations

parse_json accepts only a single recipient. For multi-recipient messages, use gose/jose/jwe_multi.

Types

Phantom type for AES-GCM Key Wrap algorithms (A128GCMKW, A192GCMKW, A256GCMKW).

pub type AesGcmKw

Phantom type for AES Key Wrap algorithms (A128KW, A192KW, A256KW).

pub type AesKw

Phantom type for JWE created via builder (new_*).

pub type Built

Phantom type for ChaCha20-Poly1305 Key Wrap algorithms (C20PKW, XC20PKW).

pub type ChaCha20Kw

A JWE decryptor that pins the expected algorithm and encryption method.

Use decryptors to prevent algorithm confusion attacks by specifying the expected algorithms upfront, rather than trusting the token’s header.

pub opaque type Decryptor

Phantom type for direct key encryption (dir).

pub type Direct

Phantom type for ECDH-ES algorithms (ECDH-ES, ECDH-ES+A*KW).

pub type EcdhEs

Phantom type for encrypted JWE (ciphertext present, can serialize/decrypt).

pub type Encrypted

A JSON Web Encryption with phantom types for state, algorithm family, and origin.

The origin phantom type distinguishes between JWE created via builders (Built) and JWE obtained by parsing tokens (Parsed). This enables compile-time enforcement that decode_*_unprotected_header only works on parsed instances.

pub opaque type Jwe(state, family, origin)

Phantom type for JWE obtained by parsing a token.

pub type Parsed

Phantom type for PBES2 algorithms (PBES2-HS*+A*KW).

pub type Pbes2

Phantom type for RSA key encryption algorithms (RSA1_5, RSA-OAEP, RSA-OAEP-256).

pub type Rsa

Phantom type for unencrypted JWE (plaintext set, ready to encrypt).

pub type Unencrypted

Values

pub fn aad(
  jwe: Jwe(Encrypted, family, origin),
) -> Result(BitArray, Nil)

Get the Additional Authenticated Data (AAD) from an encrypted JWE.

Returns Ok(aad) if AAD was set, Error(Nil) if not. AAD is only present in JSON serialization; compact format never has AAD.

pub fn alg(
  jwe: Jwe(state, family, origin),
) -> algorithm.KeyEncryptionAlg

Get the key encryption algorithm (alg) from a JWE.

pub fn cty(
  jwe: Jwe(state, family, origin),
) -> Result(String, Nil)

Get the content type (cty) from a JWE header.

pub fn decode_shared_unprotected_header(
  jwe: Jwe(Encrypted, family, Parsed),
  decoder: decode.Decoder(a),
) -> Result(a, gose.GoseError)

Decode the shared unprotected header using a custom decoder.

Security Warning: Shared unprotected headers are NOT integrity protected. Values can be modified by an attacker without detection. Never trust security-critical parameters from unprotected headers.

This function only works on parsed JWE instances. When building a JWE, you already know what unprotected headers you set - use has_shared_unprotected_header to check their presence.

Returns an error if no shared unprotected headers are present.

Example

let decoder = {
  use id <- decode.field("x-request-id", decode.string)
  decode.success(id)
}
let assert Ok(request_id) =
  jwe.decode_shared_unprotected_header(parsed_jwe, decoder)
pub fn decode_unprotected_header(
  jwe: Jwe(Encrypted, family, Parsed),
  decoder: decode.Decoder(a),
) -> Result(a, gose.GoseError)

Decode the per-recipient unprotected header using a custom decoder.

Security Warning: Per-recipient unprotected headers are NOT integrity protected. Values can be modified by an attacker without detection. Never trust security-critical parameters from unprotected headers.

This function only works on parsed JWE instances. When building a JWE, you already know what unprotected headers you set - use has_unprotected_header to check their presence.

Returns an error if no per-recipient unprotected headers are present.

Example

let decoder = {
  use id <- decode.field("x-recipient-id", decode.string)
  decode.success(id)
}
let assert Ok(recipient_id) =
  jwe.decode_unprotected_header(parsed_jwe, decoder)
pub fn decrypt(
  decryptor: Decryptor,
  jwe: Jwe(Encrypted, family, origin),
) -> Result(BitArray, gose.GoseError)

Decrypt a JWE using a decryptor with algorithm pinning.

This is the recommended way to decrypt JWEs as it prevents algorithm confusion attacks by validating that the token’s algorithms match the expected algorithms configured in the decryptor.

Example

// Create a decryptor that only accepts A256GCM with direct encryption
let assert Ok(decryptor) = jwe.key_decryptor(algorithm.Direct, algorithm.AesGcm(algorithm.Aes256), [key])

// This will fail if the token uses a different algorithm
let assert Ok(plaintext) = jwe.decrypt(decryptor, jwe)
pub fn enc(
  jwe: Jwe(state, family, origin),
) -> algorithm.ContentAlg

Get the content encryption algorithm (enc) from a JWE.

pub fn encrypt(
  jwe: Jwe(Unencrypted, family, Built),
  key key: key.Key(String),
  plaintext plaintext: BitArray,
) -> Result(Jwe(Encrypted, family, Built), gose.GoseError)

Encrypt a JWE using the appropriate key-based algorithm.

Dispatches to the correct key encryption method based on the algorithm selected when the JWE was created. Supports direct, AES Key Wrap, AES-GCM Key Wrap, RSA, and ECDH-ES algorithms.

For PBES2 password-based algorithms, use encrypt_with_password instead.

JWK metadata (use, key_ops) is enforced when present:

  • Keys with use=sig are rejected
  • Keys with key_ops that don’t include encrypt or wrapKey are rejected

Example

let key = key.generate_enc_key(algorithm.AesGcm(algorithm.Aes256))
let assert Ok(encrypted) = jwe.new_direct(algorithm.AesGcm(algorithm.Aes256))
  |> jwe.encrypt(key, <<"hello":utf8>>)
pub fn encrypt_with_password(
  jwe: Jwe(Unencrypted, Pbes2, Built),
  password password: String,
  plaintext plaintext: BitArray,
) -> Result(Jwe(Encrypted, Pbes2, Built), gose.GoseError)

Encrypt a JWE using a password (PBES2).

Example

let assert Ok(encrypted) = jwe.new_pbes2(algorithm.Pbes2Sha256Aes128Kw, algorithm.AesGcm(algorithm.Aes128))
  |> jwe.encrypt_with_password("super-secret", <<"hello":utf8>>)
pub fn has_shared_unprotected_header(
  jwe: Jwe(Encrypted, family, origin),
) -> Bool

Check if shared unprotected headers are present.

Returns True if the JWE was parsed from JSON with shared unprotected headers, or if shared unprotected headers were added via with_shared_unprotected.

pub fn has_unprotected_header(
  jwe: Jwe(Encrypted, family, origin),
) -> Bool

Check if per-recipient unprotected headers are present.

Returns True if the JWE was parsed from JSON with per-recipient unprotected headers, or if per-recipient unprotected headers were added via with_unprotected.

pub fn key_decryptor(
  alg: algorithm.KeyEncryptionAlg,
  enc: algorithm.ContentAlg,
  keys keys: List(key.Key(String)),
) -> Result(Decryptor, gose.GoseError)

Create a key-based decryptor for symmetric (dir, AES-KW, AES-GCM-KW) or asymmetric (RSA-OAEP, ECDH-ES) algorithms with multiple keys.

The decryptor pins the expected algorithm and encryption method. Tokens with different algorithms will be rejected.

When decrypting, keys are tried in order. If the JWE has a kid header, a key with matching kid is prioritized.

Example

let assert Ok(decryptor) = jwe.key_decryptor(algorithm.Direct, algorithm.AesGcm(algorithm.Aes256), [key])
let assert Ok(plaintext) = jwe.decrypt(decryptor, encrypted_jwe)
pub fn kid(
  jwe: Jwe(state, family, origin),
) -> Result(String, Nil)

Get the key ID (kid) from a JWE header.

pub fn new_aes_gcm_kw(
  size: algorithm.AesKeySize,
  enc: algorithm.ContentAlg,
) -> Jwe(Unencrypted, AesGcmKw, Built)

Create a new unencrypted JWE for AES-GCM Key Wrap encryption. A random CEK is generated and wrapped using AES-GCM with the provided symmetric key.

Example

let assert Ok(encrypted) = jwe.new_aes_gcm_kw(algorithm.Aes256, algorithm.AesGcm(algorithm.Aes256))
  |> jwe.encrypt(key, <<"hello":utf8>>)
pub fn new_aes_kw(
  size: algorithm.AesKeySize,
  enc: algorithm.ContentAlg,
) -> Jwe(Unencrypted, AesKw, Built)

Create a new unencrypted JWE for AES Key Wrap encryption. A random CEK is generated and wrapped with the provided symmetric key.

Example

let assert Ok(encrypted) = jwe.new_aes_kw(algorithm.Aes256, algorithm.AesGcm(algorithm.Aes256))
  |> jwe.encrypt(key, <<"hello":utf8>>)
pub fn new_chacha20_kw(
  variant: algorithm.ChaCha20Kw,
  enc: algorithm.ContentAlg,
) -> Jwe(Unencrypted, ChaCha20Kw, Built)

Create a new unencrypted JWE for ChaCha20-Poly1305 Key Wrap encryption. A random CEK is generated and wrapped using ChaCha20-Poly1305 or XChaCha20-Poly1305 with the provided 32-byte symmetric key.

This is a non-standard extension (not defined in RFC 7518).

Example

let assert Ok(encrypted) = jwe.new_chacha20_kw(algorithm.XC20PKw, algorithm.AesGcm(algorithm.Aes256))
  |> jwe.encrypt(key, <<"hello":utf8>>)
pub fn new_direct(
  enc: algorithm.ContentAlg,
) -> Jwe(Unencrypted, Direct, Built)

Create a new unencrypted JWE for direct key encryption. The symmetric key is used directly as the Content Encryption Key (CEK).

Example

let assert Ok(encrypted) = jwe.new_direct(algorithm.AesGcm(algorithm.Aes256))
  |> jwe.encrypt(key, <<"hello":utf8>>)
pub fn new_ecdh_es(
  alg: algorithm.EcdhEsAlg,
  enc: algorithm.ContentAlg,
) -> Jwe(Unencrypted, EcdhEs, Built)

Create a new unencrypted JWE for ECDH-ES key agreement. An ephemeral key pair is generated during encryption for the key agreement.

Example

let assert Ok(encrypted) = jwe.new_ecdh_es(algorithm.EcdhEsDirect, algorithm.AesGcm(algorithm.Aes256))
  |> jwe.encrypt(key, <<"hello":utf8>>)
pub fn new_pbes2(
  alg: algorithm.Pbes2Alg,
  enc: algorithm.ContentAlg,
) -> Jwe(Unencrypted, Pbes2, Built)

Create a new unencrypted JWE for PBES2 password-based encryption. The CEK is derived from the password using PBKDF2.

Use with_p2c to override the default iteration count. The salt is generated automatically.

Example

let assert Ok(encrypted) = jwe.new_pbes2(algorithm.Pbes2Sha256Aes128Kw, algorithm.AesGcm(algorithm.Aes128))
  |> jwe.encrypt_with_password("secret", <<"hello":utf8>>)
pub fn new_rsa(
  alg: algorithm.RsaEncryptionAlg,
  enc: algorithm.ContentAlg,
) -> Jwe(Unencrypted, Rsa, Built)

Create a new unencrypted JWE for RSA key encryption. A random CEK is generated and encrypted with the RSA public key.

Example

let assert Ok(encrypted) = jwe.new_rsa(algorithm.RsaOaepSha256, algorithm.AesGcm(algorithm.Aes256))
  |> jwe.encrypt(rsa_key, <<"hello":utf8>>)
pub fn parse_compact(
  token: String,
) -> Result(Jwe(Encrypted, Nil, Parsed), gose.GoseError)

Parse a JWE from compact format.

Returns an encrypted JWE that can be decrypted. Uses Nil family since algorithm family isn’t known at compile time.

Example

let assert Ok(parsed) = jwe.parse_compact(token)
let assert Ok(decryptor) = jwe.key_decryptor(algorithm.Direct, algorithm.AesGcm(algorithm.Aes256), [key])
let assert Ok(plaintext) = jwe.decrypt(decryptor, parsed)
pub fn parse_json(
  json_str: String,
) -> Result(Jwe(Encrypted, Nil, Parsed), gose.GoseError)

Parse a JWE from JSON format (supports both General and Flattened).

pub fn password_decryptor(
  alg: algorithm.Pbes2Alg,
  enc: algorithm.ContentAlg,
  password password: String,
) -> Decryptor

Create a password-based decryptor for PBES2 algorithms.

The decryptor pins the expected algorithm and encryption method. Tokens with different algorithms will be rejected.

Example

let decryptor = jwe.password_decryptor(
  algorithm.Pbes2Sha256Aes128Kw,
  algorithm.AesGcm(algorithm.Aes128),
  "super-secret",
)
let assert Ok(plaintext) = jwe.decrypt(decryptor, encrypted_jwe)
pub fn serialize_compact(
  jwe: Jwe(Encrypted, family, Built),
) -> Result(String, gose.GoseError)

Serialize an encrypted JWE to compact format.

Format: {protected}.{encrypted_key}.{iv}.{ciphertext}.{tag}

Returns an error if AAD is set, since compact format does not support AAD. Use serialize_json_flattened or serialize_json_general for JWEs with AAD.

Example

let assert Ok(token) = jwe.serialize_compact(encrypted)
// -> "eyJhbGci...ciphertext...tag"
pub fn serialize_json_flattened(
  jwe: Jwe(Encrypted, family, Built),
) -> json.Json

Serialize an encrypted JWE to JSON Flattened format.

Format: {"protected":"...","encrypted_key":"...","iv":"...","ciphertext":"...","tag":"..."}

For Direct or ECDH-ES algorithms, the encrypted_key field is omitted. When AAD is present, includes the aad field. When unprotected headers are present, includes the unprotected and/or header fields.

For multiple recipients, use gose/jose/jwe_multi.

pub fn serialize_json_general(
  jwe: Jwe(Encrypted, family, Built),
) -> json.Json

Serialize an encrypted JWE to JSON General format.

Format: {"protected":"...","recipients":[{"encrypted_key":"..."}],"iv":"...","ciphertext":"...","tag":"..."}

For Direct or ECDH-ES algorithms, the encrypted_key field is omitted. When AAD is present, includes the aad field. When unprotected headers are present, includes the unprotected field and/or the header field in the recipient object.

For multiple recipients, use gose/jose/jwe_multi.

pub fn typ(
  jwe: Jwe(state, family, origin),
) -> Result(String, Nil)

Get the type (typ) from a JWE header.

pub fn with_aad(
  jwe: Jwe(Unencrypted, family, Built),
  aad: BitArray,
) -> Jwe(Unencrypted, family, Built)

Set the Additional Authenticated Data (AAD) for JSON serialization.

AAD is only supported in JSON serialization (flattened and general formats). Attempting to serialize to compact format with AAD set will return an error.

pub fn with_apu(
  jwe: Jwe(Unencrypted, EcdhEs, Built),
  apu: BitArray,
) -> Jwe(Unencrypted, EcdhEs, Built)

Set the Agreement PartyUInfo (apu) for ECDH-ES algorithms.

Example

let jwe = jwe.new_ecdh_es(algorithm.EcdhEsDirect, algorithm.AesGcm(algorithm.Aes256))
  |> jwe.with_apu(<<"Alice":utf8>>)
  |> jwe.with_apv(<<"Bob":utf8>>)
let assert Ok(encrypted) = jwe
  |> jwe.encrypt(key, <<"hello":utf8>>)
pub fn with_apv(
  jwe: Jwe(Unencrypted, EcdhEs, Built),
  apv: BitArray,
) -> Jwe(Unencrypted, EcdhEs, Built)

Set the Agreement PartyVInfo (apv) for ECDH-ES algorithms.

Example

let jwe = jwe.new_ecdh_es(algorithm.EcdhEsDirect, algorithm.AesGcm(algorithm.Aes256))
  |> jwe.with_apu(<<"Alice":utf8>>)
  |> jwe.with_apv(<<"Bob":utf8>>)
let assert Ok(encrypted) = jwe
  |> jwe.encrypt(key, <<"hello":utf8>>)
pub fn with_cty(
  jwe: Jwe(Unencrypted, family, Built),
  cty: String,
) -> Jwe(Unencrypted, family, Built)

Set the content type (cty) header parameter.

pub fn with_kid(
  jwe: Jwe(Unencrypted, family, Built),
  kid: String,
) -> Jwe(Unencrypted, family, Built)

Set the key ID (kid) header parameter.

pub fn with_p2c(
  jwe: Jwe(Unencrypted, Pbes2, Built),
  iterations: Int,
) -> Result(Jwe(Unencrypted, Pbes2, Built), gose.GoseError)

Set the PBES2 iteration count (p2c) for password-based encryption.

This allows customizing the PBKDF2 iteration count. Production should use a value tuned for the specific use case.

Returns an error if iterations is less than 1,000 or greater than 10,000,000.

Example

let assert Ok(jwe) =
  jwe.new_pbes2(algorithm.Pbes2Sha256Aes128Kw, algorithm.AesGcm(algorithm.Aes128))
  |> jwe.with_p2c(100_000)
pub fn with_shared_unprotected(
  jwe: Jwe(Unencrypted, family, Built),
  name name: String,
  value value: json.Json,
) -> Result(Jwe(Unencrypted, family, Built), gose.GoseError)

Add a shared unprotected header parameter.

Security Warning: Shared unprotected headers are NOT integrity protected. They can be modified by an attacker without detection.

Returns an error if the name is a protected-only header (alg, enc, crit, zip) which must be integrity protected.

Shared unprotected headers apply to all recipients in JSON serialization. Compact serialization will return an error if unprotected headers are present.

If the same header name is set multiple times, the last value wins.

Example

let assert Ok(jwe) =
  jwe.new_direct(algorithm.AesGcm(algorithm.Aes256))
  |> jwe.with_shared_unprotected("x-request-id", json.string("abc-123"))
pub fn with_typ(
  jwe: Jwe(Unencrypted, family, Built),
  typ: String,
) -> Jwe(Unencrypted, family, Built)

Set the type (typ) header parameter (e.g., “JWT”).

pub fn with_unprotected(
  jwe: Jwe(Unencrypted, family, Built),
  name name: String,
  value value: json.Json,
) -> Result(Jwe(Unencrypted, family, Built), gose.GoseError)

Add a per-recipient unprotected header parameter.

Security Warning: Per-recipient unprotected headers are NOT integrity protected. They can be modified by an attacker without detection.

Returns an error if the name is a protected-only header (alg, enc, crit, zip) which must be integrity protected.

Per-recipient headers appear in JSON serialization only and apply to the single recipient. Compact serialization will return an error if unprotected headers are present.

If the same header name is set multiple times, the last value wins.

Search Document