About

The library provides an easy way to create deterministically encrypted and authenticated integer id fields for Ecto schemas. The fields can be used to hide actual integer PK values from the outside world by replacing them with encrypted versions (for example, in URLs).

Why

Using encryption on-the-fly is a great alternative to UUID primary keys in your database - spending CPU cycles of your application servers in most cases is much better than wasting storage and memory of the DB server. Also UUIDs spread like a virus through foreign keys, unless you create a separate integer field for references, which is a waste too.

The library mostly targets Postgres and you can encrypt any integer column Postgres supports (normal and autoincrement, signed and unsigned).

When using the library resource URLs may look like this: https://example.com/posts/GLWsqG8DwIUxd7MecoTzDPg0fSLDN74qEyYy9Dw82SInd77vSi2Ops

Installation

The package can be installed by adding ecto_encrypted_id to your list of dependencies in mix.exs:

def deps do
  [
    {:ecto_encrypted_id, "~> 0.1"}
  ]
end

Prepare a 32bytes encryption key. If you are using Phoenix:

$ mix phx.gen.secret 32

By default the library will try to use secret key provided in the application environment. You can configure it in runtime.exs like this, for example:

config :ecto_encrypted_id, :settings,
  secret_key: System.fetch_env!("SECRET")

If you don't like to use application environment, you can inject a function providing the key (we will discuss it later).

Usage

For every Ecto schema that will use an encrypted id in your project you should consider creating a separate module. The name of the module doesn't matter, but it's better to name your modules in a way that it's easy to understand which Id module corresponds to which schema. Let's say you have a Post schema:

defmodule MyProject.PostId do
  use EctoEncryptedId, salt: "my salt"
end

Note the salt parameter: it must be a string that is unique between field modules, so that different models (schemas) don't share the same encrypted ids.

If you prefer to provide secret key in a different way, not by using app environment, you can pass a function returning the key like this:

use EctoEncryptedId, salt: "my salt", secret_key_fn: &MyProject.Secret.key/0

After that, add the field as a primary key for the schema. This example is using autoincrementing integer PK:

defmodule MyProject.Post do
  use Ecto.Schema

  @primary_key {:id, MyProject.PostId, autogenerate: true}
  schema "posts" do
    field :title, :string
  end
end

Now, every time you load the schema instance from the DB, id field will contain an instance of MyProject.PostId.Id. See the EctoEncryptedId.ExampleField.Id module docs to get an idea on how to use it.

Also, if you are using Phoenix, consider adding Phoenix.Param implementation, to be able to use your id in URLs, same way like you could with integers. If you want to use encrypted versions:

defmodule MyProject.Post do
  ...
  defimpl Phoenix.Param, for: __MODULE__ do
    defdelegate to_param(term), to: EctoEncryptedId, as: :encrypted_param
  end
  ...
end

Or if you want to keep the unencryped versions (not that it makes much sense, but anyway):

defmodule MyProject.Post do
  ...
  defimpl Phoenix.Param, for: __MODULE__ do
    defdelegate to_param(term), to: EctoEncryptedId, as: :plain_param
  end
  ...
end

The library provides Phoenix.HTML.Safe implementation that outputs encrypted versions in HTML automatically. This way you won't accidentally leak plain text ids.

Important Considerations

It may take a bit of time getting used to non-scalar id in models. The library doesn't try to make important decisions implicitly - as a developer you will have to decide whether to use encrypted version or integer one, on a case-by-case basis. The additional decryption/encryption step for URLs may seem annoying, and maybe it is, but very often the advantages are worth the trouble.

The encryption we use is deterministic - the encrypted id is defined by the secret key and the salt. As long as you don't change them you can use the encrypted ids in permalinks.

Also, as we discussed earlier, every field module should use different salt. The salt is not secret, but the encryption key is.

Don't use existing secret keys from your project as the key for this library - in case there's been a possible leak, you should always change secrets that guard real security stuff, like passwords and bank accounts, but in the case of this library, you could even prefer to keep using the leaked key just to avoid breaking the permalinks. It's not like we are guarding government secrets here, right ? RIGHT ?

The encryption is intended to be strong enough for most cases (unless the attacker is a 3-letter agency). But if you know something about cryptography your feedback would be welcome.