Compact RFC 7516 encryption with explicitly pinned algorithms and AES GCM.
encrypt/4 takes ordered protected header pairs with alg/enc, plaintext
bytes, and a PublicKey or arity-two wrapper (wire_algorithm, generated_cek).
The wrapper returns {:ok, %{encrypted_key: bytes, header: additions_map}};
additions are protected, cannot replace caller members, and may contain only
iv/tag for GCMKW. Protected headers use compact JSON: top-level members keep
caller order, followed by additions sorted by member name; nested JSON objects
are serialized in map order. For JWS signatures needing exact protected bytes,
use RequestSeal.JOSE.JWS.sign_protected/4. For dir, the wrapper must
provision the generated CEK to its recipient through caller-owned custody
and return an empty encrypted key.
No symmetric secret can be supplied in a public key or header. Only
timeout: milliseconds is accepted (default 5,000; 1–300,000).
decrypt/2 requires exactly :algorithms, :encryption, :max_plaintext
(1–1,048,576), :timeout (1–300,000), and :key_resolver. Algorithms are
unique nonempty lists selected from RSA-OAEP-256, RSA-OAEP, A128GCMKW,
A256GCMKW, dir; content encryption is A128GCM or A256GCM. The arity-one
resolver receives %{algorithm: name, encryption: name, header: map} and
returns {:ok, %{algorithm: same_name, unwrap: arity_two_fun}},
{:ok, %{algorithm: same_name, key: custody_handle}}, or :error.
A custody handle must bind {:jwe, same_name} with :unwrap capability;
RSA-OAEP private operations stay inside its sensitive local holder. The
existing callback path can still use caller-owned raw key material.
Unwrap receives (encrypted_key_bytes, header) and returns {:ok, cek} or
{:error, term}. Resolver and callback unwrap execute in JWE's sensitive worker.
On the handle path, the CEK passes from the sensitive local holder through a
separate sensitive custody runner and middle process to that JWE worker.
These four processes see the CEK; JWE's outer middle process and the decrypt
caller receive only the authenticated result. All work shares one absolute
deadline. Unwrap failure uses a fresh random CEK and
still attempts tag authentication; both paths return :decryption_failed.
A released or unavailable resolved handle makes decryption fail with the same
complete error as a forged message, by design. Check
RequestSeal.Custody.public_key(handle) at startup: {:ok, public_key} confirms
public resolution; {:error, error} reports custody availability separately
from message processing. Handle algorithm/capability mismatch also follows the
unwrap-failure path. Resolver algorithm mismatch remains a key-selection error,
and deadline expiration remains distinct.
CEK/IV use the real CSPRNG. Decrypted data stays in the private worker until
full authentication succeeds; no partial result is returned. IVs are 12 bytes,
tags 16 bytes, CEKs exactly 16/32 bytes. The protected segment is verbatim AAD.
Returns {:ok, compact} / {:ok, Result} or {:error, JOSE.Error}.
Summary
Functions
Authenticate and decrypt a compact JWE under an explicit policy (RFC 7516).
Encrypt plaintext into compact JWE with fresh CEK and IV bytes (RFC 7516).
Functions
@spec decrypt(binary(), map()) :: {:ok, RequestSeal.JOSE.JWE.Result.t()} | {:error, RequestSeal.JOSE.Error.t()}
Authenticate and decrypt a compact JWE under an explicit policy (RFC 7516).
The policy must be a map containing exactly these atom keys:
:algorithms: a nonempty unique list selected from"RSA-OAEP-256","RSA-OAEP","A128GCMKW","A256GCMKW", and"dir".:encryption: a nonempty unique list selected from"A128GCM"and"A256GCM".:max_plaintext: an integer from 1 through 1,048,576 bytes.:timeout: an integer from 1 through 300,000 milliseconds.:key_resolver: an arity-one function receiving%{algorithm: wire_name, encryption: wire_name, header: decoded_header_map}. It returns:error,{:ok, %{algorithm: same_name, unwrap: arity_two_function}}, or{:ok, %{algorithm: same_name, key: custody_handle}}, with exactly two entry keys. Unwrap receives(encrypted_key_bytes, header)and returns{:ok, cek_bytes}or{:error, term}. Thekey:handle form works only for"RSA-OAEP"and"RSA-OAEP-256", binding{:jwe, same_name}with only:unwrapcapability; RSA private operations stay in custody."dir","A128GCMKW", and"A256GCMKW"require theunwrap:callback. A handle supplied for one of those algorithms returns:decryption_failed.
Resolution, unwrapping, and authentication share one absolute deadline with
sensitive workers and caller cancellation. No plaintext leaves the worker
before full tag authentication. Returns {:ok, RequestSeal.JOSE.JWE.Result.t()}
or {:error, RequestSeal.JOSE.Error.t()}. Invalid policies return :invalid_policy;
unwrap and authentication failures share :decryption_failed; expiry remains
:deadline_exceeded. Compact input is bounded to 1,048,576 bytes. The received
protected segment is used verbatim as AAD. Recipient integrity establishes no
sender identity, request association, claims validation, or authorization.
This is a local RFC 7518 round trip, using real key wrapping and AEAD:
iex> wrapping_key = {:aes, :crypto.strong_rand_bytes(32)}
iex> wrap = fn algorithm, cek -> RequestSeal.JOSE.KeyManagement.wrap(algorithm, cek, %{}, wrapping_key) end
iex> {:ok, token} = RequestSeal.JOSE.JWE.encrypt([{"alg", "A256GCMKW"}, {"enc", "A256GCM"}], "payload", wrap)
iex> unwrap = fn bytes, header -> RequestSeal.JOSE.KeyManagement.unwrap("A256GCMKW", bytes, header, wrapping_key) end
iex> policy = %{algorithms: ["A256GCMKW"], encryption: ["A256GCM"], max_plaintext: 1_024, timeout: 5_000, key_resolver: fn _ -> {:ok, %{algorithm: "A256GCMKW", unwrap: unwrap}} end}
iex> {:ok, result} = RequestSeal.JOSE.JWE.decrypt(token, policy)
iex> result.plaintext
"payload"
iex> {:error, error} = RequestSeal.JOSE.JWE.decrypt(token, Map.delete(policy, :encryption))
iex> error.reason
:invalid_policy
@spec encrypt( RequestSeal.JOSE.header(), binary(), RequestSeal.PublicKey.t() | function(), keyword() ) :: {:ok, binary()} | {:error, RequestSeal.JOSE.Error.t()}
Encrypt plaintext into compact JWE with fresh CEK and IV bytes (RFC 7516).
header is an ordered list of {string_name, json_value} pairs with "alg"
selected from "RSA-OAEP-256", "RSA-OAEP", "A128GCMKW", "A256GCMKW",
or "dir", and "enc" selected from "A128GCM" or "A256GCM".
Members must be unique. "crit" rejects as :unsupported_critical_header,
"zip" as :compression_unsupported, and "jku", "x5u", "jwk", and
"b64" as :unsupported_header, regardless of their values.
Plaintext and the resulting compact envelope are bounded to 1,048,576 bytes.
recipient is a compatible RSA RequestSeal.PublicKey or an arity-two function
receiving (wire_algorithm, generated_cek_bytes). The function returns
{:ok, %{encrypted_key: bytes, header: additions_map}} with exactly those keys.
Additions may contain only "iv"/"tag" for GCM key wrapping, cannot replace
caller members, and become authenticated protected headers. For "dir", the
function provisions the generated CEK through caller-owned custody and returns
an empty encrypted key. Symmetric secrets cannot be supplied as public keys.
opts accepts only timeout: milliseconds (default 5,000; integer 1–300,000).
This function accepts no policy map; decryption selects its own explicit policy.
Wrapping and encryption run in a sensitive worker with deadline and caller
cancellation. Returns {:ok, compact_binary} or
{:error, RequestSeal.JOSE.Error.t()}. Error reasons are :invalid_options,
:invalid_serialization, :limit, :invalid_header, :duplicate_member,
:unsupported_critical_header, :compression_unsupported, :unsupported_header,
:algorithm_not_permitted, :algorithm_mismatch, :invalid_base64,
:invalid_iv, :invalid_tag, :decryption_failed, :entropy_failure,
:signer_failed, and :deadline_exceeded. Callback failure or an invalid wrap
result returns :signer_failed; malformed encrypted-key bytes return
:decryption_failed. This function has no key resolver and does not return
:key_resolver_failed. Only :deadline_exceeded is retryable.
See the module documentation for protected JSON serialization order.
This example uses actual RFC 7518 AES GCM key wrapping and content encryption:
iex> wrapping_key = {:aes, :crypto.strong_rand_bytes(32)}
iex> wrap = fn algorithm, cek -> RequestSeal.JOSE.KeyManagement.wrap(algorithm, cek, %{}, wrapping_key) end
iex> {:ok, token} = RequestSeal.JOSE.JWE.encrypt([{"alg", "A256GCMKW"}, {"enc", "A256GCM"}], "payload", wrap, timeout: 5_000)
iex> is_binary(token) and length(String.split(token, ".")) == 5
true
iex> {:error, error} = RequestSeal.JOSE.JWE.encrypt([{"alg", "A256GCMKW"}, {"enc", "A256GCM"}], "payload", wrap, timeout: 0)
iex> error.reason
:invalid_options