lib/gpg.ex

# GPGMEx - Native Elixir bindings for GnuPG
# Copyright (C) 2022  Matt Silbernagel
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
defmodule GPG do
  @moduledoc """
  Native [GnuPG](https://gnupg.org/) bindings

  > ### Warning {: .warning}
  >
  > This is still a work in progress and the API is likely to
  > change. It is not considered producion quality yet.

  > ### Warning {: .error}
  >
  > This has only been tested on Linux - It likely won't work for
  > Mac OSX or Windows yet.

  ## Getting Started

  You'll need:
  * a working version of [gpg](https://gnupg.org/) installed
  * [gpgme c library](https://gnupg.org/related_software/gpgme/index.html)
  * configuration added to `config.exs` 

  ### Debian based (ubuntu, pop-os, etc)

  **Installing gpg and gpgme**

  ```bash
  $ sudo apt install gpg libgpgme-dev
  ```

  **Configuration**

  Add this to `config.exs` in your app

  ```elixir
  config :gpgmex,
    gpg_home: "~/.gnupg",    # where your gpg home path is
    gpg_path: "/usr/bin/gpg" # where your gpg binary lives
  ```

  ### Arch based (Arch, Manjaro, etc)

  **Installing gpg and gpgme**

  ```bash
  $ sudo pacman -Syu gpg gpgme
  ```

  **Configuration**

  Add this to `config.exs` in your app

  ```elixir
  config :gpgmex,
    gpg_home: "~/.gnupg",    # where your gpg home path is
    gpg_path: "/usr/bin/gpg" # where your gpg binary lives
  ```

  ## Add to your Dependencies

  Add gpgmex to your dependencies
  ```elixir
  defp deps do
    [
      {:gpgmex, "~> 0.0.12"}
    ]
  end
  ```
  """

  @doc """
  Get the currently installed GPG library version

  ## Examples

      iex> GPG.get_engine_version()
      "1.17.1"
  """
  @spec get_engine_version() :: String.t() | :error
  def get_engine_version do
    version = GPG.NativeAPI.check_version()
    to_string(version)
  catch
    _e -> :error
  end

  @doc """
  Get information about the currently installed GPG library

  ## Examples

      iex> GPG.get_engine_info()
      %{bin: "/usr/bin/gpg", directory: "~/.gnupg"}
  """
  @spec get_engine_info() :: map() | :error
  def get_engine_info() do
    GPG.NativeAPI.engine_info()
  catch
    _e -> :error
  end

  @doc """
  Get the fingerprint of the public key for an email if the public key is on your system

  ## Examples

      iex> GPG.get_public_key("matt@silbernagel.dev")
      {:ok, "80C8F7AE64E589449FB0A03974DB6708422DD33B"}
  """
  @spec get_public_key(binary()) :: {:ok, binary()} | :error
  def get_public_key(email) do
    GPG.NativeAPI.public_key(email)
  catch
    _e -> :error
  end

  @doc """
  Encrypt data for the requested email recipient

  This works for any public key you have on your system.
  If you don't have the key on your system `{:error, :keynoexist}`
  is returned

  ## Examples

      iex> GPG.encrypt("matt@silbernagel.dev", "encrypt this text")
      {:ok, "-----BEGIN PGP MESSAGE-----\\n\\nhQIMA1M1Dqrc4va7AQ/"}
  """
  @spec encrypt(String.t(), binary()) :: {:ok, binary()} | {:error, atom()}
  def encrypt(email, data) do
    GPG.NativeAPI.encrypt(email, data)
  catch
    _e -> {:error, :unknown}
  end

  @doc """
  Decrypt the given data.

  This only works if you have the
  private key available on your system that matches the 
  public key that encrypted it

  ## Examples

      iex> GPG.decrypt("-----BEGIN PGP MESSAGE-----\\n\\nww8K2o8JL1ejKjJSOte0RmhLl6V7M6KW7p9D4Y1zHobTxVnGlmW64wxuWJx03Xs5\\nqymK+m7aUrAO0HL3vri3R2z1SisrUAeAtI/4v3GUWA00g4Q0rPzibDe3m53VkY7/\\nlyAzJSXL29LL93IJezx53GRK9+RYSBULYWLI3NPX10zidwKbnz+8jo41TIOx0SNh\\nt6aAyErC4pnepy7xq7IdWzSe/7v+lrcYpyGT35jyeR+e4N7N7SJV/+WQ+RxBQ/TS\\nPwHkMaec6aIgfLTt/lCryJFPEv02C5v0JQg8jJ7SjSH2FOk1y4HPIOJC/qatlLZq\\ntDiu13SA0+UBilW1j4AhXA==\\n=CXnG\\n-----END PGP MESSAGE-----\\n")
      {:ok, "data"}
  """
  @spec decrypt(binary()) :: {:ok, binary()} | {:error, binary()}
  def decrypt(data) do
    GPG.NativeAPI.decrypt(data)
  catch
    e -> {:error, to_string(e)}
  end

  @doc """
  Clearsigning causes the data to be wrapped in an ASCII-armored signature but otherwise does not modify the data.

  This allows recipients of the data to verify that it is indeed from the correct person.

  ## Examples
      iex> GPG.clear_sign("data")
      {:ok, "-----BEGIN PGP SIGNED MESSAGE-----\\nHash: SHA512\\n\\ndata\\n-----BEGIN PGP SIGNATURE-----\\n\\niQIzBAEBCgAdFiEEgMj3rmTliUSfsKA5dNtnCEIt0zsFAmQPzq0ACgkQdNtnCEIt\\n0zvsmw/+JZWfHhbHgqy9lw11QuagovqV0HQdk9C/wrzbrmeAP8g+AvkDDbo2GTP7\\neHOfOaWJDCD6qWvSt//JIs8khQfnQ3faBhPunQt+iPze1N9JSKTbJway3fJKr5dQ\\nyFAjFDt/AHFCGUzE37eld/TE+ehsj3H7fTxAe9GdPWM3r3n9MpggzCb5YQYSk7yy\\nYdWOWIhbyVt7RTk4hzuNh4wWaprQvuU38saDMMkZbHUxR0oIIoomfgsywLdb0HZA\\n8iGvex7uqyWPHCY2NMpdSJ4E0xBNURwarlHE32/sRZrISAMfW/nWY4tTWFHN8Spz\\ncBDclyzFkwjihMz/+9Dl4VfTN7UQuFh3/4Z12dl0RS9d1sz45bVcNy5DapArviOj\\nmaAzvYyodWQ8qthWZDT+ZAPCIky61gVLkcxqXArTamoxQbxBsLkGrNx2Up8caYBK\\nPH6o8XuIXTb640jzpOgPSL63qfn3HgvZr/9nyyhrZv3ASroSOCcLgvBaxl4MZ0pN\\nKnKJnklhCKdKcz2as+KPpWGXA7WKY5s/7JQdZDdSA2zYHwirNI0qaZ5UFgkyJWzJ\\ncu+v/ZjVgeidPKCD65Yn3UIY2wXWTqDcI5sSWXFTHnVljEeC16yjuzYWXgvYLDrM\\n0ypPbndz7WBckg5UKukAWPwQl0P61zBmywx13UZ1/9cww7Gp9Jw=\\n=MgoU\\n-----END PGP SIGNATURE-----\\n"}
  """
  @spec clear_sign(binary()) :: {:ok, binary()} | {:error, binary()}
  def clear_sign(data) do
    GPG.NativeAPI.clear_sign(data)
  catch
    e -> {:error, to_string(e)}
  end

  @doc """
  Verifys the clear signed data.

  ## Examples
      iex> GPG.verify_clear("-----BEGIN PGP SIGNED MESSAGE-----\\nHash: SHA512\\n\\ndata\\n-----BEGIN PGP SIGNATURE-----\\n\\niQIzBAEBCgAdFiEEgMj3rmTliUSfsKA5dNtnCEIt0zsFAmQPzq0ACgkQdNtnCEIt\\n0zvsmw/+JZWfHhbHgqy9lw11QuagovqV0HQdk9C/wrzbrmeAP8g+AvkDDbo2GTP7\\neHOfOaWJDCD6qWvSt//JIs8khQfnQ3faBhPunQt+iPze1N9JSKTbJway3fJKr5dQ\\nyFAjFDt/AHFCGUzE37eld/TE+ehsj3H7fTxAe9GdPWM3r3n9MpggzCb5YQYSk7yy\\nYdWOWIhbyVt7RTk4hzuNh4wWaprQvuU38saDMMkZbHUxR0oIIoomfgsywLdb0HZA\\n8iGvex7uqyWPHCY2NMpdSJ4E0xBNURwarlHE32/sRZrISAMfW/nWY4tTWFHN8Spz\\ncBDclyzFkwjihMz/+9Dl4VfTN7UQuFh3/4Z12dl0RS9d1sz45bVcNy5DapArviOj\\nmaAzvYyodWQ8qthWZDT+ZAPCIky61gVLkcxqXArTamoxQbxBsLkGrNx2Up8caYBK\\nPH6o8XuIXTb640jzpOgPSL63qfn3HgvZr/9nyyhrZv3ASroSOCcLgvBaxl4MZ0pN\\nKnKJnklhCKdKcz2as+KPpWGXA7WKY5s/7JQdZDdSA2zYHwirNI0qaZ5UFgkyJWzJ\\ncu+v/ZjVgeidPKCD65Yn3UIY2wXWTqDcI5sSWXFTHnVljEeC16yjuzYWXgvYLDrM\\n0ypPbndz7WBckg5UKukAWPwQl0P61zBmywx13UZ1/9cww7Gp9Jw=\\n=MgoU\\n-----END PGP SIGNATURE-----\\n")
      {:ok, "data\\n"}
  """
  @spec verify_clear(binary()) :: {:ok, binary()} | {:error, binary()}
  def verify_clear(data) do
    GPG.NativeAPI.verify_clear(data)
  catch
    e -> {:error, to_string(e)}
  end

  @doc """
  Generate a GPG key using the provided email address.

  This generates a new GPG using rsa3072 encryption. It will
  use the system prompt to ask for a password.

  ## Examples

      iex> GPG.generate_key("my_new@email.com")
      :ok
  """
  @spec generate_key(String.t()) :: :ok | :error
  def generate_key(email) do
    GPG.NativeAPI.generate_key(email)
  catch
    _e -> :error
  end

  @doc """
  Delete an existing GPG key
  """
  @spec delete_key(binary()) :: number() | :error
  def delete_key(email) do
    GPG.NativeAPI.delete_key(email)
  catch
    _e -> :error
  end

  @doc """
  Import a public key
  """
  @spec import_key(binary()) :: :ok | {:error, binary()}
  def import_key(data) do
    GPG.NativeAPI.import_key(data)
  end

  @doc """
  Gets data about a public key
  """
  @spec key_info(binary()) :: map() | {:error, binary()}
  def key_info(public_key) do
    GPG.NativeAPI.key_info(public_key)
  end

  @doc """
  List all known keys on the system.
  """
  @spec list_keys() :: [map()]
  def list_keys() do
    GPG.NativeAPI.list_keys()
  end
end