RequestSeal.JOSE.JWE (RequestSeal v0.1.0)

Copy Markdown View Source

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

decrypt(compact, policy)

@spec decrypt(binary(), map()) ::
  {:ok, RequestSeal.JOSE.JWE.Result.t()} | {:error, RequestSeal.JOSE.Error.t()}

encrypt(header, plaintext, recipient, opts \\ [])

@spec encrypt(
  RequestSeal.JOSE.header(),
  binary(),
  RequestSeal.PublicKey.t() | function(),
  keyword()
) ::
  {:ok, binary()} | {:error, RequestSeal.JOSE.Error.t()}