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:
- JWK
algmetadata: If a key hasalgset viakey.with_alg, the JWE algorithm must match during encryption and decryption. - Decryptor API:
jwe.decrypt()with aDecryptorpins both key encryption and content encryption algorithms; mismatches are rejected. - 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:
- Empty arrays are rejected
- Standard headers cannot appear in
crit - No extensions are currently implemented, so any critical extension is rejected
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 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 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 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=sigare rejected - Keys with
key_opsthat don’t includeencryptorwrapKeyare 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.