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 decryption, assuming 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.