# `RequestSeal.JOSE.KeyManagement`
[🔗](https://github.com/baselabs/request_seal/blob/v0.5.2/lib/request_seal/jose/key_management.ex#L1)

Custodian-side RFC 7518 key wrapping primitives over OTP.

`wrap/4` and `unwrap/4` consume operation-local descriptors only:
`{:rsa, otp_key}`, `{:aes, kek}`, `{:cek, bytes}`. RSA wrapping takes
an OTP public key; unwrapping takes an OTP private key.
Private keys and symmetric material remain the caller custodian's responsibility.

RSA-OAEP uses SHA-1/MGF1 SHA-1; RSA-OAEP-256 uses SHA-256/MGF1 SHA-256,
both with an empty label. A128GCMKW/A256GCMKW use AES GCM with empty AAD.
JOSE envelopes enforce the content cipher's CEK width; primitives also support
published unwrap stages belonging to other content algorithms without enabling
those algorithms.

`wrap/4` reports a descriptor or KEK that does not fit the algorithm as
`:algorithm_mismatch`. `unwrap/4` reports a KEK of the wrong size as
`:algorithm_mismatch` (rule `jose/key.algorithm_mismatch` in the
cross-language conformance corpus) and every other failure to recover a CEK,
including a descriptor of the wrong kind, as `:decryption_failed`. These
primitive reasons never reach a decrypting peer: `RequestSeal.JOSE.JWE.decrypt/2`
replaces any unwrap failure with a random CEK and returns the same
`:decryption_failed` as an authentication failure.

# `unwrap`

```elixir
@spec unwrap(binary(), binary(), map(), tuple()) ::
  {:ok, binary()} | {:error, RequestSeal.JOSE.Error.t()}
```

Recover a content-encryption key (CEK) from its wrapped form (RFC 7518).

Received widths reject before OTP is called. Use it inside a
`RequestSeal.JOSE.JWE.decrypt/2` `unwrap:` callback, which supplies the
encrypted key and decoded protected header.

## Parameters

  * `alg` - `"RSA-OAEP-256"`, `"RSA-OAEP"`, `"A128GCMKW"`, `"A256GCMKW"`, or
    `"dir"`.
  * `bytes` - the encrypted key, a binary of at most 1,048,576 bytes. RSA
    requires exactly the modulus width; `"dir"` requires `""`.
  * `header` - the decoded protected header map. GCM key wrapping reads its
    `"iv"` (12 bytes) and `"tag"` (16 bytes), canonical unpadded Base64url.
  * `material` - the descriptor matching `alg`:
    * `{:rsa, rsa_private_key}` - OTP `:RSAPrivateKey` record, two-prime,
      2048 to 8192 bits, CRT components checked.
    * `{:aes, kek}` - 16-byte KEK for `"A128GCMKW"`, 32-byte KEK for
      `"A256GCMKW"`.
    * `{:cek, key}` - for `"dir"`; a 16- or 32-byte key, returned as the CEK.

## Returns

  * `{:ok, cek}` - the unwrapped bytes. This function does not check the width
    against a content cipher; `RequestSeal.JOSE.JWE.decrypt/2` does.
  * `{:error, %RequestSeal.JOSE.Error{}}` - reasons this function can return:
    `:algorithm_not_permitted`, `:invalid_header` (non-map header),
    `:invalid_serialization` (non-binary input), `:limit`, `:algorithm_mismatch`
    (KEK of the wrong size), `:invalid_base64`, `:invalid_iv`, `:invalid_tag`,
    and `:decryption_failed` (descriptor does not fit `alg`, invalid private
    key, wrong encrypted-key width, or authentication failure). Meanings:
    `RequestSeal.JOSE.Error`.

## Examples

    iex> kek = :crypto.strong_rand_bytes(32)
    iex> cek = :crypto.strong_rand_bytes(32)
    iex> {:ok, %{encrypted_key: wrapped, header: header}} = RequestSeal.JOSE.KeyManagement.wrap("A256GCMKW", cek, %{}, {:aes, kek})
    iex> RequestSeal.JOSE.KeyManagement.unwrap("A256GCMKW", wrapped, header, {:aes, kek}) == {:ok, cek}
    true
    iex> {:error, error} = RequestSeal.JOSE.KeyManagement.unwrap("A256GCMKW", wrapped, header, {:aes, :crypto.strong_rand_bytes(32)})
    iex> error.reason
    :decryption_failed
    iex> {:error, error} = RequestSeal.JOSE.KeyManagement.unwrap("A256GCMKW", wrapped, %{}, {:aes, kek})
    iex> error.reason
    :invalid_base64

# `wrap`

```elixir
@spec wrap(binary(), binary(), map(), tuple()) ::
  {:ok, map()} | {:error, RequestSeal.JOSE.Error.t()}
```

Wrap a content-encryption key (CEK) for one recipient (RFC 7518).

The result is the shape a `RequestSeal.JOSE.JWE.encrypt/4` wrapper returns.

## Parameters

  * `alg` - `"RSA-OAEP-256"`, `"RSA-OAEP"`, `"A128GCMKW"`, `"A256GCMKW"`, or
    `"dir"`.
  * `bytes` - the CEK, a binary of at most 1,048,576 bytes. RSA-OAEP limits it
    further by the modulus size.
  * `header` - a map; not read when wrapping (pass `%{}`).
  * `material` - the descriptor matching `alg`:
    * `{:rsa, {:RSAPublicKey, n, e}}` - OTP RSA public key, 2048 to 8192 bits,
      for `"RSA-OAEP"` and `"RSA-OAEP-256"`.
    * `{:aes, kek}` - 16-byte KEK for `"A128GCMKW"`, 32-byte KEK for
      `"A256GCMKW"`.
    * `{:cek, key}` - for `"dir"`; `key` must equal `bytes`.

## Returns

  * `{:ok, %{encrypted_key: bytes, header: additions}}` - RSA returns a
    modulus-width encrypted key and `%{}`; GCM key wrapping returns the wrapped
    key and `%{"iv" => b64, "tag" => b64}` (fresh 12-byte IV, 16-byte tag, which
    must be protected); `"dir"` returns `""` and `%{}`.
  * `{:error, %RequestSeal.JOSE.Error{}}` - reasons this function can return:
    `:algorithm_not_permitted`, `:invalid_header` (non-map header),
    `:invalid_serialization` (non-binary CEK), `:limit`, `:algorithm_mismatch`
    (descriptor does not fit `alg`, wrong KEK size, or `"dir"` key differs),
    `:entropy_failure`, and `:signer_failed` (invalid RSA key or OTP failure).
    Meanings: `RequestSeal.JOSE.Error`.

## Examples

    iex> kek = :crypto.strong_rand_bytes(16)
    iex> cek = :crypto.strong_rand_bytes(16)
    iex> {:ok, %{encrypted_key: wrapped, header: header}} = RequestSeal.JOSE.KeyManagement.wrap("A128GCMKW", cek, %{}, {:aes, kek})
    iex> {byte_size(wrapped), Enum.sort(Map.keys(header))}
    {16, ["iv", "tag"]}
    iex> {:error, error} = RequestSeal.JOSE.KeyManagement.wrap("A256GCMKW", cek, %{}, {:aes, kek})
    iex> error.reason
    :algorithm_mismatch

---

*Consult [api-reference.md](api-reference.md) for complete listing*
