At work, we occasionally have a need to store credentials for third party integrations in the database. We’ve developed a scheme for encrypting these plaintext values when writing to the database and decrypting the ciphertext when reading from the database. This is process happens automatically using ActiveRecord before_save and after_find callbacks; from the perspective of callers, the Integration#password always contains a plaintext password.

(It is important to note here that these are integrations with external services and we need to be able to recover the plaintext credentials in order to authenticate with them. There is no need to recover the plaintext of user password used for logging into your own system; you should only store salted, hashed passwords in those situations.)

I have been playing around with moving some of those integrations to Elixir and was struggling to find a clean way to accomplish the same encrypt/decrypt functionality in Elixir. My first approach was to add a decrypt_password function to the Integration module (I’m not concerned with writing encrypted data yet, just reading it from the database). This approach did not feel like the right one, though. While the remaining fields are directly available on the Integration structure, accessing a password will require a different interaction. Accessing the column’s value directly without going through decrypt_password will return gibberish, and even if I always remember to use that function, retrieving an encrypted value will require special case code. Requiring a function call means the decrypted value cannot be accessed as a normal field on the struct and functions like Map.take/2 won’t be able to grab the decrypted value.

When searching around for a different approach (maybe a virtual field derived from the encrypted column?) I came across a post on the Elixir forum with a suggestion to use a custom type. Custom types in Ecto provide a clean interface for marshalling a data structure between Elixir and the database. In this case, the Elixir representation of the data will be the plaintext password, while the database representation of the data will be the ciphertext.

The Ecto docs give a good overview of how to implement a custom type, in short a module needs to adopt the Ecto.Type behaviour and define cast/1, dump/1, load/1 and type/0 functions. Since we’re first concerned with reading from the database, load/1 is our focus. That function receives an “Ecto native” type, meaning something analogous to what is stored in the database (:string, :integer, :boolean, etc.) and returns the custom type representing that value. In our case, load/1 will receive the ciphertext string stored in the database, decrypt it and return the plaintext. The encrypted field is represented in the database as a JSON hash with three Base64 encoded values. Our module, then, starts to look like this:

defmodule EncryptedString do
  @behaviour Ecto.Type

  def load(raw) when is_binary(raw) do
    %{"iv" => iv,
      "auth_tag" => auth_tag,
      "cipher_text" => ciphertext} = Jason.decode!(raw)
    {:ok, SomeCryptoModule.decrypt(Base.decode64!(iv),
                                   Base.decode64!(auth_tag),
                                   Base.decode64!(ciphertext)) }
  end
end

And when we declare a field as an EncryptedString in a schema, we see that field automatically decrypted for us when it’s read from the database:

defmodule Integration do
  use Ecto.Schema

  schema "integrations" do
    ...
    field :username, EncryptedString
    field :password, EncryptedString
    ...
  end
end

iex(1)> Integration |> Ecto.Query.first |> Repo.one

%Integration{
  ...
  password: "secret password"
  username: "username"
  ...
}

In the end, we’ve achieved the same behavior that ActiveRecord’s callbacks gave us, but our EncryptedString module is far more testable than AR callbacks are. Merely constructing a test case for the AR callback approach is challenging, because the callbacks are coupled to the database. We need some way to get a properly encrypted ciphertext into the database before we can verify that the callback is able to decrypt it. Because Ecto custom types are powered by functions, we don’t even need to involve the database. Passing a properly encrypted ciphertext to load/1 should return the plaintext, and when things are further fleshed out we can assert plaintext |> dump |> load == plaintext.

Like so many things in Elixir, I started out trying to mimic Ruby/Rails behavior and wound up in a much better spot thanks to functions.