lib/cryppo.ex

defmodule Cryppo do
  @moduledoc """
    Main public API of Cryppo
  """

  import Cryppo.Strategies, only: [find_strategy: 1, find_key_derivation_strategy: 1]

  alias Cryppo.{
    DerivedKey,
    EncryptedData,
    EncryptedDataWithDerivedKey,
    EncryptionKey,
    RsaSignature,
    Serialization,
    Strategies
  }

  @typedoc """
  Name of an encryption or derivation strategy

  Use `Cryppo.encryption_strategies/0` to get a list of encryption strategies.
  Use `Cryppo.derivation_strategies/0` to get a list of derivation strategies.
  """

  @type encryption_strategy() :: String.t()

  @typedoc """
  Module of an encryption or derivation strategy
  """
  @type encryption_strategy_module() :: atom

  @doc "List available encryption strategies"
  @spec encryption_strategies :: [encryption_strategy()]
  def encryption_strategies, do: Strategies.encryption_strategies()

  @doc "List available  derivation strategies"
  @spec derivation_strategies :: [encryption_strategy()]
  def derivation_strategies, do: Strategies.derivation_strategies()

  @doc """
  Generate an encryption key for an encryption strategy

  The generated encrypted key is marked as belonging to the encryption strategy.

  ## Example

      iex> _encryption_key = Cryppo.generate_encryption_key("Aes256Gcm")
  """
  @spec generate_encryption_key(encryption_strategy) ::
          EncryptionKey.t() | {:unsupported_encryption_strategy, binary}
  def generate_encryption_key(encryption_strategy) when is_binary(encryption_strategy) do
    with {:ok, mod} <- find_strategy(encryption_strategy) do
      %EncryptionKey{} = apply(mod, :generate_key, [])
    end
  end

  @doc """
  Encrypt data with an encryption key

  ## Example

      iex> encryption_key = Cryppo.generate_encryption_key("Aes256Gcm")
      iex> _encrypted_data = Cryppo.encrypt("data to encrypt", "Aes256Gcm", encryption_key)

  The encryption key must match the encryption strategy:

      iex> encryption_key = Cryppo.generate_encryption_key("Aes256Gcm")
      iex> Cryppo.encrypt("data to encrypt", "Rsa4096", encryption_key)
      {:incompatible_key, [submitted_key_strategy: Cryppo.Aes256gcm, encryption_strategy: Cryppo.Rsa4096]}
  """
  @spec encrypt(binary, encryption_strategy, EncryptionKey.t() | any) ::
          EncryptedData.t()
          | {:unsupported_encryption_strategy, atom}
          | {:error, :invalid_encryption_key}
          | :encryption_error
          | {:incompatible_key, submitted_key_strategy: atom, encryption_strategy: atom}
  def encrypt(data, encryption_strategy, encryption_key_or_raw_key)
      when is_binary(encryption_strategy) and is_binary(data) do
    with {:ok, mod} <- find_strategy(encryption_strategy) do
      encryption_key_or_raw_key =
        encryption_key_or_raw_key
        |> add_encryption_strategy_module(mod)

      apply(mod, :run_encryption, [data, encryption_key_or_raw_key])
    end
  end

  @doc """
  Generate an encryption key for an encryption strategy and encrypt data with this encryption key

  ## Example

      iex> {_encrypted_data, _encryption_key} = Cryppo.encrypt("data to encrypt", "Aes256Gcm")
  """
  @spec encrypt(binary, encryption_strategy) ::
          EncryptedData.t()
          | {:unsupported_encryption_strategy, atom}
          | :encryption_error
          | {:encryption_error, any}
  def encrypt(data, encryption_strategy)
      when is_binary(encryption_strategy) and is_binary(data) do
    with {:ok, mod} <- find_strategy(encryption_strategy) do
      encryption_key = apply(mod, :generate_key, [])
      encrypted = apply(mod, :run_encryption, [data, encryption_key])
      {encrypted, encryption_key}
    end
  end

  defp add_encryption_strategy_module(key, mod) do
    case key do
      %EncryptionKey{encryption_strategy_module: nil} = key ->
        %{key | encryption_strategy_module: mod}

      k ->
        k
    end
  end

  @doc """
  Decrypt encrypted data with an encryption key

  ## Example

      iex> {encrypted_data, encryption_key} = Cryppo.encrypt("data to encrypt", "Aes256Gcm")
      iex> Cryppo.decrypt(encrypted_data, encryption_key)
      {:ok, "data to encrypt"}
  """
  @spec decrypt(EncryptedData.t(), EncryptionKey.t() | any) ::
          {:ok, binary}
          | {:error, :invalid_encryption_key}
          | :decryption_error
          | {:decryption_error, {any, any}}
          | {:incompatible_key, submitted_key_strategy: atom, encryption_strategy: atom}
  def decrypt(
        %EncryptedData{encryption_strategy_module: mod} = encrypted_data,
        encryption_key_or_raw_key
      ) do
    encryption_key_or_raw_key = encryption_key_or_raw_key |> add_encryption_strategy_module(mod)

    apply(mod, :run_decryption, [encrypted_data, encryption_key_or_raw_key])
  end

  @doc """
  Encrypt data with a derived key

  ## Example

      iex> _encrypted = Cryppo.encrypt_with_derived_key("data to encrypt", "Aes256Gcm", "Pbkdf2Hmac", "passphrase")

  """
  @spec encrypt_with_derived_key(binary, encryption_strategy(), encryption_strategy(), String.t()) ::
          EncryptedDataWithDerivedKey.t()
          | {:unsupported_encryption_strategy, encryption_strategy}
          | {:unsupported_key_derivation_strategy, encryption_strategy}
  def encrypt_with_derived_key(data, encryption_strategy, key_derivation_strategy, passphrase)
      when is_binary(encryption_strategy) and is_binary(key_derivation_strategy) and
             is_binary(passphrase) and is_binary(data) do
    with {:ok, key_derivation_mod} <- find_key_derivation_strategy(key_derivation_strategy),
         {:ok, encryption_strategy_mod} <- find_strategy(encryption_strategy) do
      if apply(encryption_strategy_mod, :key_derivation_possible, []) do
        key_length = apply(encryption_strategy_mod, :key_length, [])

        %DerivedKey{encryption_key: key} =
          derived_key = apply(key_derivation_mod, :generate_derived_key, [passphrase, key_length])

        key_with_encryption_strategy = %{
          key
          | encryption_strategy_module: encryption_strategy_mod
        }

        %EncryptedData{} =
          encrypted_data =
          apply(encryption_strategy_mod, :run_encryption, [data, key_with_encryption_strategy])

        %EncryptedDataWithDerivedKey{encrypted_data: encrypted_data, derived_key: derived_key}
      else
        {:encryption_strategy_does_not_support_key_derivation, encryption_strategy}
      end
    end
  end

  @doc """
  Decrypt data with a derived key

  ## Example

      iex> encrypted = Cryppo.encrypt_with_derived_key("data to encrypt", "Aes256Gcm", "Pbkdf2Hmac", "passphrase")
      iex> {:ok, decrypted, _key} = Cryppo.decrypt_with_derived_key(encrypted, "passphrase")
      iex> decrypted
      "data to encrypt"
  """
  @spec decrypt_with_derived_key(EncryptedDataWithDerivedKey.t(), String.t()) ::
          {:ok, binary, DerivedKey.t()}
          | :decryption_error
          | {:decryption_error, {any, any}}
          | {:incompatible_key, submitted_key_strategy: atom, encryption_strategy: atom}
  def decrypt_with_derived_key(
        %EncryptedDataWithDerivedKey{
          derived_key: %DerivedKey{key_derivation_strategy: key_derivation_mod} = derived_key,
          encrypted_data:
            %EncryptedData{encryption_strategy_module: encryption_strategy_mod} = encrypted_data
        },
        passphrase
      )
      when is_binary(passphrase) do
    derived_key =
      %DerivedKey{encryption_key: key} =
      apply(key_derivation_mod, :build_derived_key, [passphrase, derived_key])

    key_with_encryption_strategy = %{key | encryption_strategy_module: encryption_strategy_mod}

    with {:ok, decrypted} <-
           apply(encryption_strategy_mod, :run_decryption, [
             encrypted_data,
             key_with_encryption_strategy
           ]) do
      {:ok, decrypted, derived_key}
    end
  end

  @doc """
  Serialize various Cryppo data structures as a string

  3 Cryppo data structures have their own serialization formats:

  * `Cryppo.EncryptedData`
  * `Cryppo.EncryptedDataWithDerivedKey`
  * `Cryppo.RsaSignature`

  ## Examples

  `Cryppo.EncryptedData`:

      iex> {encrypted_data, _key} = Cryppo.encrypt("data to encrypt", "Aes256Gcm")
      iex> Cryppo.serialize(encrypted_data)

  `Cryppo.EncryptedDataWithDerivedKey`:

      iex> "data to encrypt"
      ...> |> Cryppo.encrypt_with_derived_key("Aes256Gcm", "Pbkdf2Hmac", "passphrase")
      ...> |> Cryppo.serialize()

  `Cryppo.RsaSignature`:

      iex> private_key = Cryppo.generate_encryption_key("Rsa4096")
      iex> "data to encrypt"
      ...> |> Cryppo.Rsa4096.sign(private_key)
      ...> |> Cryppo.serialize()
  """
  @spec serialize(EncryptedData.t() | EncryptedDataWithDerivedKey.t() | RsaSignature.t()) ::
          binary
  def serialize(s)
  def serialize(%EncryptedData{} = s), do: Serialization.serialize(s)
  def serialize(%EncryptedDataWithDerivedKey{} = s), do: Serialization.serialize(s)
  def serialize(%RsaSignature{} = s), do: Serialization.serialize(s)

  @doc """
  Load various Cryppo data structures from their serialized forms

  3 Cryppo data structures have their own serialization formats:

  * `Cryppo.EncryptedData`
  * `Cryppo.EncryptedDataWithDerivedKey`
  * `Cryppo.RsaSignature`

  ## Examples

      iex> s = "Aes256Gcm.WSDb2AmsF7LFOxYb.QUAAAAACYWQABQAAAG5vbmUABWF0ABAAAAAAY9Ck6LzVGiMdiWFK6N5BawVpdgAMAAAAAG_Yxh-I0gGNYoFRigA="
      iex> {:ok, %Cryppo.EncryptedData{}} = Cryppo.load(s)

      iex> s = "Aes256Gcm.wW4M_sv_kMx14cC6.QUAAAAACYWQABQAAAG5vbmUABWF0ABAAAAAA8Aq84t28sMT9FL8cz-TmMQVpdgAMAAAAANahbwbkfWo18YuCMgA=.Pbkdf2Hmac.SzAAAAAQaQAJUwAABWl2ABQAAAAAuQTqZLVFO49lI6Kx454ffYQ9VV0QbAAgAAAAAA=="
      iex> {:ok, %Cryppo.EncryptedDataWithDerivedKey{}} = Cryppo.load(s)

      iex> s = "Sign.Rsa4096.V4JbRzpkud-3cHCGqDwGjS3TmRto5Te0iSAtD7oIzsDa83McBDYpU_eeswVZF9AGEvoAEQOCwpqJ_PgbjHKT2nHgLysK-btG6Nxk_K2J7A6Uq15X5QrOgIKTzC00dj1tzAN73u9lsRPKIfwPyp_Mlb6FNs1LoB7OvAusit6QPm8iAwHo4nOWBBUf3hO9b3gsWJ92FxnBsCLYFQj_zv4mnLHj7pDNVtq9Kp4hK6bgcIH4FZtyDKDr6bXEtlCGLDIY10UqNLylkagI36Gyafm-HnD57vRxjgHIGEsd2XcwDJ8PqqrzSYNxl-RyWD3wq0nXE_1rYJ7k1AKLM5G1Hg8B2whqcXpQ52x3zVFCAjlU9GNhT6pdUBxQYw09va7fe2w517PrwwMe90MW87fj3G7dGEKT95cDLTx1d84ybIUFUJOGKY0FF4LL0E3UqWQ92kU4bh-DSTkNmgItX34fiBIOpQDbF238IkRYyFA8LfMPfL-0_dnto9sH0E3Umi41qFvpA2Nq8r57FF4vCOSkXYWVfyitOkY_URqMLxS57azwZRBehJYDtvbqmzaYEDceeLjkxDi--Y10LT4Cz2SGiU--YDJM66PZ3Cp74gvDpsWlohcwYmMib5LrjdtvLOAtOZhoLZyGeeX0lDnwOum7lFRpJd8UIrOlTvpBo48ep2bpmgA=.VmVyaMO8dHVuZyB2ZXJib3Rlbg=="
      iex> {:ok, %Cryppo.RsaSignature{}} = Cryppo.load(s)
  """
  @spec load(binary) ::
          {:ok, EncryptedDataWithDerivedKey.t() | EncryptedData.t() | RsaSignature.t()}
          | {:error,
             :invalid_base64
             | :invalid_bson
             | :invalid_derivation_artefacts
             | :invalid_serialization_value
             | :invalid_encryption_artefacts
             | String.t()}
          | {:unsupported_encryption_strategy, binary}
          | {:unsupported_key_derivation_strategy, binary}
  def load(serialized) when is_binary(serialized) do
    case String.split(serialized, ".") do
      ["Sign", "Rsa4096", signature, data] ->
        RsaSignature.load(signature, data)

      [strategy_name, encrypted_data, encryption_artefacts] ->
        EncryptedData.load(strategy_name, encrypted_data, encryption_artefacts)

      [
        strategy,
        encrypted_data,
        encryption_artefacts,
        key_derivation_strategy,
        derivation_artefacts
      ] ->
        EncryptedDataWithDerivedKey.load(
          strategy,
          encrypted_data,
          encryption_artefacts,
          key_derivation_strategy,
          derivation_artefacts
        )

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