lib/nostr_basics/crypto/aes_256_cbc.ex

defmodule NostrBasics.Crypto.AES256CBC do
  @moduledoc """
  Algorithm that encrypts and decrypts direct messages
  """

  @doc """
  Encrypts a direct message

  The example below can't test for the result, as it changes on every execution...

  ## Examples
      iex> remote_public_key = <<0xEFC83F01C8FB309DF2C8866B8C7924CC8B6F0580AFDDE1D6E16E2B6107C2862C::256>>
      ...> local_private_key = <<0x4E22DA43418DD934373CBB38A5AB13059191A2B3A51C5E0B67EB1334656943B8::256>>
      ...> "this message will end up being encrypted"
      ...> |> NostrBasics.Crypto.AES256CBC.encrypt(local_private_key, remote_public_key)
  """
  @spec encrypt(String.t(), <<_::256>>, <<_::256>>) :: String.t()
  def encrypt(message, seckey, pubkey) do
    iv = :crypto.strong_rand_bytes(16)

    shared_secret = get_shared_secret(seckey, pubkey)

    cipher_text =
      :crypto.crypto_one_time(:aes_256_cbc, shared_secret, iv, message,
        encrypt: true,
        padding: :pkcs_padding
      )

    b64_cypher_text = Base.encode64(cipher_text)
    b64_iv = Base.encode64(iv)

    "#{b64_cypher_text}?iv=#{b64_iv}"
  end

  @doc """
  Decrypts a direct message

  ## Examples
      iex> remote_public_key = <<0xEFC83F01C8FB309DF2C8866B8C7924CC8B6F0580AFDDE1D6E16E2B6107C2862C::256>>
      ...> local_private_key = <<0x4E22DA43418DD934373CBB38A5AB13059191A2B3A51C5E0B67EB1334656943B8::256>>
      ...> "sWLtVbabr8fzIugnOXo4og==?iv=nxF/xVqbC4JdMRUEC0Jfyg=="
      ...> |> NostrBasics.Crypto.AES256CBC.decrypt(local_private_key, remote_public_key)
      {:ok, "test"}
  """
  @spec decrypt(String.t(), <<_::256>>, <<_::256>>) ::
          {:ok, String.t()} | {:error, atom() | String.t()}
  def decrypt(message, seckey, pubkey) do
    [message, iv] = String.split(message, "?iv=")

    with {:ok, message} <- Base.decode64(message),
         {:ok, iv} <- Base.decode64(iv) do
      shared_secret = get_shared_secret(seckey, pubkey)

      decrypted =
        :crypto.crypto_one_time(:aes_256_cbc, shared_secret, iv, message,
          encrypt: false,
          padding: :pkcs_padding
        )

      {:ok, decrypted}
    else
      {:error, message} -> {:error, message}
      :error -> {:error, "cannot decode iv, which should be in base64"}
    end
  end

  defp get_shared_secret(<<_::256>> = seckey, <<_::256>> = pubkey) do
    :crypto.compute_key(
      :ecdh,
      <<0x02::8, pubkey::bitstring-256>>,
      seckey,
      :secp256k1
    )
  end
end