Sheharyar Naseer

Encrypting Data in Elixir with Plug.Crypto


It’s not uncommon to encrypt arbitrary values in an application for various reasons (such as passing state in an OAuth flow), and for the most part I’ve been using the Cipher package in Elixir for that purpose.

But unfortunately Cipher has not been maintained for a couple of years and a few security issues have also surfaced (for example it using a static initialization vector), so we decided to replace it with another crypto library. A while back, we asked the Dashbit team for a alternative recommendation and José suggested we wait for the Phoenix team to release v1.5 which will include Plug.Crypto and various helpers for better security.

The wait was worth it, as the transition from Cipher to Plug.Crypto was seamless. We did, however, used a small wrapper around it to make it simpler to use while still employing best practices:

defmodule MyApp.Crypto do
  @default_ttl 1 * 60 * 60 # 1 hour

  @doc "Encrypt any Erlang term"
  @spec encrypt(atom, any) :: binary
  def encrypt(context, term) do
    Plug.Crypto.encrypt(secret(), to_string(context), term)
  end

  @doc "Decrypt cipher-text into an Erlang term"
  @spec decrypt(atom, binary) :: {:ok, any} | {:error, atom}
  def decrypt(context, ciphertext, max_age \\ @default_ttl) when is_binary(ciphertext) do
    Plug.Crypto.decrypt(secret(), to_string(context), ciphertext, max_age: max_age)
  end

  defp secret, do: MyApp.Web.Endpoint.config(:secret_key_base)
end

While we have re-used the secret_key_base value specified to Phoenix here as the encryption secret, you can use any other value, although you should take care to load them for the appropriate env config instead of hardcoding it.

During encryption, we specify the context (or the “type” of encrypted token if you will) so that the encrypted value can only be decrypted by the part of your application that’s meant to decrypt it:

cipher = Crypto.encrypt(:auth, "my secret")
# => ...

Crypto.decrypt(:auth, cipher)
# => {:ok, "my secret"}

Crypto.decrypt(:api, cipher)
# => {:error, :invalid}

We’ve also used a default age of 1 hour so when not explicitly overridden, tokens older than that will error during decrypting, suggesting replay attacks.

Another unexpected benefit of switching to Plug.Crypto was its underlying use of :erlang.term_to_binary which allows encrypting and decrypting any term or struct without manually handling it ourselves as we previously had to do with Cipher:

state_config = %Config{a: nil, b: "word", c: {:ok, [1, 2]}, d: %{"x" => false}}
cipher = Crypto.encrypt(:integration, state_config)

Crypto.decrypt(:integration, cipher)
# => {:ok, %Config{a: nil, b: "word", ...}}
# (We get the exact same Erlang/Elixir term back)

So far, I’m very happy with the decision to move to Plug.Crypto and would also suggest to other Elixir developers to use this over poorly or unmaintained packages that could introduce unexpected security issues in your application.