lib/cryppo/rsa4096.ex

defmodule Cryppo.Rsa4096 do
  @moduledoc """
  Encryption strategy RSA with 4096-bit keys and some RSA-specific functions

  For encryption and decryption please use functions in module `Cryppo`.
  This module also contains logic for PEMs, singing and verification.

  """

  # Key length 4096
  # Exponents: 65537
  # Padding: rsa_pkcs1_oaep_padding

  use Cryppo.EncryptionStrategy,
    strategy_name: "Rsa4096",
    # 4096 is the key size in ruby Cryppo
    key_length: 4_096,
    key_derivation_possible: false

  alias Cryppo.RsaSignature

  @typedoc """
  Erlang type for RSA private keys

  The native Erlang type for RSA private keys in module [`public_key`](https://erlang.org/doc/man/public_key.html)
  are Erlang records visible from Elixir as tuples with 11 terms the first term being atom `:RSAPrivateKey`
  """
  @type rsa_private_key() ::
          {:RSAPrivateKey, integer, integer, integer, integer, integer, integer, integer, integer,
           integer, any}

  @typedoc """
  Erlang type for RSA public keys

  The native Erlang type for RSA public keys in module [`public_key`](https://erlang.org/doc/man/public_key.html)
  are Erlang records visible from Elixir as tuples with 3 terms the first term being atom `:RSAPublicKey`
  """
  @type rsa_public_key() :: {:RSAPublicKey, integer, integer}

  @typedoc """
  RSA keys in PEM format
  """
  @type pem() :: String.t()

  # 65537 is the default in OpenSSL, and hence in ruby Cryppo
  @exponent 65_537

  # rsa_pkcs1_oaep_padding is the padding in ruby Cryppo
  @padding :rsa_pkcs1_oaep_padding

  @spec generate_key :: EncryptionKey.t()
  @impl true
  def generate_key do
    {:rsa, key_length(), @exponent}
    |> :public_key.generate_key()
    |> EncryptionKey.new(__MODULE__)
  end

  @spec encrypt(binary, EncryptionKey.t()) ::
          {:ok, binary, EncryptionArtefacts.t()} | :encryption_error
  @impl EncryptionStrategy
  def encrypt(data, %EncryptionKey{key: private_key})
      when is_binary(data) and elem(private_key, 0) == :RSAPrivateKey and
             tuple_size(private_key) == 11 do
    public_key = private_key_to_public_key(private_key)
    encrypt(data, EncryptionKey.new(public_key, __MODULE__))
  end

  def encrypt(data, %EncryptionKey{key: public_key})
      when is_binary(data) and elem(public_key, 0) == :RSAPublicKey and
             tuple_size(public_key) == 3 do
    encrypted = data |> :public_key.encrypt_public(public_key, rsa_padding: @padding)
    {:ok, encrypted, %EncryptionArtefacts{}}
  rescue
    _e in ErlangError ->
      {:encryption_error,
       "the input data to encrypt is likely bigger than Rsa4096 + rsa_pkcs1_oaep_padding can handle"}

    e ->
      e
  end

  def encrypt(_, _), do: :encryption_error

  @doc """
  Extracts a public key from a private key

  Extracts a public key from a `Cryppo.EncryptionKey` struct with an RSA private key or from an
  RSA private key in the native Erlang type `t:rsa_private_key/0`

  ## Examples

  With a `Cryppo.EncryptionKey` struct:

      iex> public_key = "Rsa4096"
      ...> |> Cryppo.generate_encryption_key()
      ...> |> Cryppo.Rsa4096.private_key_to_public_key()
      ...> elem(public_key, 0)
      :RSAPublicKey

  With a native Erlang key:

      iex> public_key = {:rsa, 4_096, 65_537}
      ...> |> :public_key.generate_key()
      ...> |> Cryppo.Rsa4096.private_key_to_public_key()
      ...> elem(public_key, 0)
      :RSAPublicKey

  """

  @spec private_key_to_public_key(rsa_private_key() | EncryptionKey.t()) :: rsa_public_key()
  def private_key_to_public_key(%EncryptionKey{
        encryption_strategy_module: __MODULE__,
        key: private_key
      }),
      do: private_key_to_public_key(private_key)

  def private_key_to_public_key(private_key)
      when is_tuple(private_key) and elem(private_key, 0) == :RSAPrivateKey and
             tuple_size(private_key) == 11 do
    public_modulus = private_key |> elem(2)
    public_exponent = private_key |> elem(3)
    {:RSAPublicKey, public_modulus, public_exponent}
  end

  @doc """
  Converts an RSA key to PEM format.

  Can convert

  * a `Cryppo.EncryptionKey` struct
  * a public key as native Erlang type `t:rsa_public_key/0`
  * a private key as native Erlang type `t:rsa_private_key/0`

  ## Examples

  With a `Cryppo.EncryptionKey` struct

      iex> "Rsa4096" |> Cryppo.generate_encryption_key() |> Cryppo.Rsa4096.to_pem()


  With a public key as native Erlang type `t:rsa_public_key/0`

      iex> "Rsa4096"
      ...> |> Cryppo.generate_encryption_key()
      ...> |> Cryppo.Rsa4096.private_key_to_public_key()
      ...> |> Cryppo.Rsa4096.to_pem()

  With a private key as native Erlang type `t:rsa_private_key/0`

      iex> encryption_key = Cryppo.generate_encryption_key("Rsa4096")
      iex> Cryppo.Rsa4096.to_pem(encryption_key.key)

  """

  @spec to_pem(EncryptionKey.t() | rsa_private_key() | rsa_public_key()) :: {:ok, pem()}
  def to_pem(%EncryptionKey{key: key}),
    do: to_pem(key)

  def to_pem(key)
      when is_tuple(key) and (elem(key, 0) == :RSAPrivateKey or elem(key, 0) == :RSAPublicKey) do
    pem_entry = key |> elem(0) |> :public_key.pem_entry_encode(key)
    {:ok, :public_key.pem_encode([pem_entry])}
  end

  @doc """
  Loads and initializes a `Cryppo.EncryptionKey` struct from a string with a PEM.

  ## Examples

      iex> pem = "-----BEGIN RSA PRIVATE KEY-----\\n" <>
      ...>       "MIICWwIBAAKBgQDKCUh7F4p5btzcSLBaToHvD3rCZX4fMaDtjkN5TwmC3/6iQzD5\\n" <>
      ...>       "tn396BzDTdQ16HuuZ+eN+KQSa1QWr2h1DB13nVP+moeyLVC8BShiM3NBRn77r7Lr\\n" <>
      ...>       "sWooM3mwnSvMPWWnBj1c+0tbO7zfur5wQdzBl66HrHgHt+Bz6f+dDj+aVwIDAQAB\\n" <>
      ...>       "AoGAMHh3rihgrW9+h07dGF1baOoyzm6hCoTSkguefn0K0B5DLdSm7FHu+jp0pBqI\\n" <>
      ...>       "/gHvolEFSZdMbarYOrUMf4BPlRSarCjjxf/beV4Pj/UQrCkDmNBBVJp33Sy8HEdb\\n" <>
      ...>       "Wrzk+k8NcAS1UR4R6EW9JrUz0mMwX6CsvG2zZMbpS/Q9KXkCQQDwmCXjOTPQ+bxW\\n" <>
      ...>       "K4gndHnXD5QkKNcTdFq64ef23R6AY0XEGkiRLDXZZA09hDIACgSSfk1Qbo0SJSvU\\n" <>
      ...>       "TAR8A6clAkEA1vkWJ5qUo+xuIZB+2604LRco1GYAj5/fZ2kvUMjbOdCFgFaDVzJY\\n" <>
      ...>       "X2pzLkk7RZNgPvXcRAgX7FlWmm4jwZzQywJARrHeSCMRx7DqF0PZUQaXmorYU7uw\\n" <>
      ...>       "XuYMluc0WsRkZwNEh7fVZNrhw8vzXAUREBPhfg4gt6aUSyWi+FGR68LDBQJAC55O\\n" <>
      ...>       "ujk6i1l94kaC9LB59sXnqQMSSLDlTBt9OSqB3rAMZxFF6/KGoDGKpBfFIk+CxiRX\\n" <>
      ...>       "kT22vUleyt3lBNPK3QJAEr56asvREcIDFkbs7Ebjev4U1PL58w78ipp49Ti5FiwH\\n" <>
      ...>       "vR9vuGcUcIDcWKOl05t4D35F5A/DskP6dGYA1cuWNg==\\n" <>
      ...>       "-----END RSA PRIVATE KEY-----\\n\\n"
      ...> {:ok, _encryption_key} = Cryppo.Rsa4096.from_pem(pem)
  """

  @spec from_pem(pem) :: {:ok, EncryptionKey.t()} | {:error, :invalid_encryption_key}
  def from_pem(pem) when is_binary(pem) do
    case :public_key.pem_decode(pem) do
      [pem_entry] ->
        encryption_key = %EncryptionKey{
          encryption_strategy_module: __MODULE__,
          key: :public_key.pem_entry_decode(pem_entry)
        }

        {:ok, encryption_key}

      _ ->
        {:error, :invalid_encryption_key}
    end
  end

  @spec decrypt(EncryptedData.t(), EncryptionKey.t()) :: {:ok, binary} | :decryption_error
  @impl EncryptionStrategy
  def decrypt(%EncryptedData{encrypted_data: encrypted_data}, %EncryptionKey{key: private_key})
      when is_binary(encrypted_data) and elem(private_key, 0) == :RSAPrivateKey and
             tuple_size(private_key) == 11 do
    decrypted = :public_key.decrypt_private(encrypted_data, private_key, rsa_padding: @padding)
    {:ok, decrypted}
  rescue
    ErlangError -> :decryption_error
  end

  def decrypt(_, _), do: :decryption_error

  @doc """
  Signs data with a private key

  The private key can be one of the following:

  * a `Cryppo.EncryptionKey` struct
  * a private key as native Erlang type `t:rsa_private_key/0`
  * a PEM with a private RSA key

  ## Examples

  With a `Cryppo.EncryptionKey` struct:

      iex> encryption_key = Cryppo.generate_encryption_key("Rsa4096")
      iex> _signature = %Cryppo.RsaSignature{} = Cryppo.Rsa4096.sign("data to sign", encryption_key)


  With a private key as native Erlang type `t:rsa_private_key/0`

      iex> private_key = :public_key.generate_key({:rsa, 4_096, 65_537})
      iex> _signature = %Cryppo.RsaSignature{} = Cryppo.Rsa4096.sign("data to sign", private_key)

  With a PEM

      iex> pem = "-----BEGIN RSA PRIVATE KEY-----\\n" <>
      ...>       "MIICWwIBAAKBgQDKCUh7F4p5btzcSLBaToHvD3rCZX4fMaDtjkN5TwmC3/6iQzD5\\n" <>
      ...>       "tn396BzDTdQ16HuuZ+eN+KQSa1QWr2h1DB13nVP+moeyLVC8BShiM3NBRn77r7Lr\\n" <>
      ...>       "sWooM3mwnSvMPWWnBj1c+0tbO7zfur5wQdzBl66HrHgHt+Bz6f+dDj+aVwIDAQAB\\n" <>
      ...>       "AoGAMHh3rihgrW9+h07dGF1baOoyzm6hCoTSkguefn0K0B5DLdSm7FHu+jp0pBqI\\n" <>
      ...>       "/gHvolEFSZdMbarYOrUMf4BPlRSarCjjxf/beV4Pj/UQrCkDmNBBVJp33Sy8HEdb\\n" <>
      ...>       "Wrzk+k8NcAS1UR4R6EW9JrUz0mMwX6CsvG2zZMbpS/Q9KXkCQQDwmCXjOTPQ+bxW\\n" <>
      ...>       "K4gndHnXD5QkKNcTdFq64ef23R6AY0XEGkiRLDXZZA09hDIACgSSfk1Qbo0SJSvU\\n" <>
      ...>       "TAR8A6clAkEA1vkWJ5qUo+xuIZB+2604LRco1GYAj5/fZ2kvUMjbOdCFgFaDVzJY\\n" <>
      ...>       "X2pzLkk7RZNgPvXcRAgX7FlWmm4jwZzQywJARrHeSCMRx7DqF0PZUQaXmorYU7uw\\n" <>
      ...>       "XuYMluc0WsRkZwNEh7fVZNrhw8vzXAUREBPhfg4gt6aUSyWi+FGR68LDBQJAC55O\\n" <>
      ...>       "ujk6i1l94kaC9LB59sXnqQMSSLDlTBt9OSqB3rAMZxFF6/KGoDGKpBfFIk+CxiRX\\n" <>
      ...>       "kT22vUleyt3lBNPK3QJAEr56asvREcIDFkbs7Ebjev4U1PL58w78ipp49Ti5FiwH\\n" <>
      ...>       "vR9vuGcUcIDcWKOl05t4D35F5A/DskP6dGYA1cuWNg==\\n" <>
      ...>       "-----END RSA PRIVATE KEY-----\\n\\n"
      ...> _signature = %Cryppo.RsaSignature{} = Cryppo.Rsa4096.sign("data to sign", pem)

  """
  @spec sign(binary, rsa_private_key() | EncryptionKey.t() | pem()) ::
          RsaSignature.t() | {:error, :invalid_encryption_key} | {:error, String.t()}

  def sign(data, _maybe_pem) when is_binary(data) and byte_size(data) > 512 do
    {:error, "cannot sign more than 512 bytes"}
  end

  def sign(data, maybe_pem) when is_binary(data) and is_binary(maybe_pem) do
    with {:ok, encryption_key} <- from_pem(maybe_pem) do
      sign(data, encryption_key)
    end
  end

  def sign(data, %EncryptionKey{encryption_strategy_module: __MODULE__, key: private_key}),
    do: sign(data, private_key)

  def sign(data, private_key_erlang_tuple)
      when is_binary(data) and is_tuple(private_key_erlang_tuple) and
             elem(private_key_erlang_tuple, 0) == :RSAPrivateKey and
             tuple_size(private_key_erlang_tuple) == 11 do
    signature = :public_key.sign(data, :sha256, private_key_erlang_tuple)
    %RsaSignature{signature: signature, data: data}
  end

  @doc """
  Verifies an RSA signature with a public key

  The key for verification can be pretty much any format and type, private keys are also accepted:

  * native Erlang types `t:rsa_private_key/0` and `t:rsa_public_key/0`
  * `Cryppo.EncryptionKey` structs
  * PEMs

    ## Examples

  With a public key in the Erlang format:

      iex> encryption_key = Cryppo.generate_encryption_key("Rsa4096")
      iex> signature = Cryppo.Rsa4096.sign("data to sign", encryption_key)
      iex> public_key = Cryppo.Rsa4096.private_key_to_public_key(encryption_key)
      iex> Cryppo.Rsa4096.verify(signature, public_key)
      true

  With a private key in the Erlang format:

      iex> encryption_key = Cryppo.generate_encryption_key("Rsa4096")
      iex> signature = Cryppo.Rsa4096.sign("data to sign", encryption_key)
      iex> Cryppo.Rsa4096.verify(signature, encryption_key.key)
      true

  With a `Cryppo.EncryptionKey` struct:

      iex> encryption_key = Cryppo.generate_encryption_key("Rsa4096")
      iex> signature = Cryppo.Rsa4096.sign("data to sign", encryption_key)
      iex> Cryppo.Rsa4096.verify(signature, encryption_key)
      true

  With a PEM

      iex> pem_with_private_key = "-----BEGIN RSA PRIVATE KEY-----\\n" <>
      ...>       "MIICWwIBAAKBgQDKCUh7F4p5btzcSLBaToHvD3rCZX4fMaDtjkN5TwmC3/6iQzD5\\n" <>
      ...>       "tn396BzDTdQ16HuuZ+eN+KQSa1QWr2h1DB13nVP+moeyLVC8BShiM3NBRn77r7Lr\\n" <>
      ...>       "sWooM3mwnSvMPWWnBj1c+0tbO7zfur5wQdzBl66HrHgHt+Bz6f+dDj+aVwIDAQAB\\n" <>
      ...>       "AoGAMHh3rihgrW9+h07dGF1baOoyzm6hCoTSkguefn0K0B5DLdSm7FHu+jp0pBqI\\n" <>
      ...>       "/gHvolEFSZdMbarYOrUMf4BPlRSarCjjxf/beV4Pj/UQrCkDmNBBVJp33Sy8HEdb\\n" <>
      ...>       "Wrzk+k8NcAS1UR4R6EW9JrUz0mMwX6CsvG2zZMbpS/Q9KXkCQQDwmCXjOTPQ+bxW\\n" <>
      ...>       "K4gndHnXD5QkKNcTdFq64ef23R6AY0XEGkiRLDXZZA09hDIACgSSfk1Qbo0SJSvU\\n" <>
      ...>       "TAR8A6clAkEA1vkWJ5qUo+xuIZB+2604LRco1GYAj5/fZ2kvUMjbOdCFgFaDVzJY\\n" <>
      ...>       "X2pzLkk7RZNgPvXcRAgX7FlWmm4jwZzQywJARrHeSCMRx7DqF0PZUQaXmorYU7uw\\n" <>
      ...>       "XuYMluc0WsRkZwNEh7fVZNrhw8vzXAUREBPhfg4gt6aUSyWi+FGR68LDBQJAC55O\\n" <>
      ...>       "ujk6i1l94kaC9LB59sXnqQMSSLDlTBt9OSqB3rAMZxFF6/KGoDGKpBfFIk+CxiRX\\n" <>
      ...>       "kT22vUleyt3lBNPK3QJAEr56asvREcIDFkbs7Ebjev4U1PL58w78ipp49Ti5FiwH\\n" <>
      ...>       "vR9vuGcUcIDcWKOl05t4D35F5A/DskP6dGYA1cuWNg==\\n" <>
      ...>       "-----END RSA PRIVATE KEY-----\\n\\n"
      ...> signature = Cryppo.Rsa4096.sign("data to sign", pem_with_private_key)
      ...> {:ok, key} = Cryppo.Rsa4096.from_pem(pem_with_private_key)
      ...> {:ok, pem_with_public_key} = key
      ...> |> Cryppo.Rsa4096.private_key_to_public_key()
      ...> |> Cryppo.Rsa4096.to_pem()
      ...> Cryppo.Rsa4096.verify(signature, pem_with_public_key)
      true
  """

  @spec verify(RsaSignature.t(), rsa_public_key | rsa_private_key | EncryptionKey.t() | pem) ::
          boolean() | {:error, :invalid_encryption_key}
  def verify(%RsaSignature{data: data, signature: signature}, public_key),
    do: verify(data, signature, public_key)

  @spec verify(binary, binary, rsa_public_key | rsa_private_key | EncryptionKey.t() | pem) ::
          boolean() | {:error, :invalid_encryption_key}

  defp verify(data, signature, maybe_pem) when is_binary(maybe_pem) do
    with {:ok, encryption_key} <- from_pem(maybe_pem),
         do: verify(data, signature, encryption_key)
  end

  defp verify(data, signature, %EncryptionKey{
         encryption_strategy_module: __MODULE__,
         key: private_key
       }),
       do: verify(data, signature, private_key)

  defp verify(data, signature, private_key)
       when is_tuple(private_key) and elem(private_key, 0) == :RSAPrivateKey do
    public_key = private_key_to_public_key(private_key)
    verify(data, signature, public_key)
  end

  defp verify(data, signature, public_key)
       when is_binary(data) and is_binary(signature) and is_tuple(public_key) and
              elem(public_key, 0) == :RSAPublicKey do
    :public_key.verify(data, :sha256, signature, public_key)
  end

  @spec build_encryption_key(any) :: {:ok, EncryptionKey.t()} | {:error, :invalid_encryption_key}
  @impl EncryptionStrategy
  def build_encryption_key(private_key_in_erlang_format)
      when is_tuple(private_key_in_erlang_format) and
             elem(private_key_in_erlang_format, 0) == :RSAPrivateKey and
             tuple_size(private_key_in_erlang_format) == 11 do
    {:ok, EncryptionKey.new(private_key_in_erlang_format, __MODULE__)}
  end

  def build_encryption_key(maybe_pem) when is_binary(maybe_pem),
    do: from_pem(maybe_pem)

  def build_encryption_key(_), do: {:error, :invalid_encryption_key}
end