Skip to content

Encrypted & authenticated integer id fields for Ecto schemas

License

Notifications You must be signed in to change notification settings

a3kov/ecto_encrypted_id

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

  • Thanks to the library it's possible to prevent resource enumeration attacks. See https://owasp.org/www-community/attacks/Forced_browsing

  • Also, we may want to use URL as a secret to access a resource. Yes, it's an example of "security by obscurity", but sometimes it's the right solution.

  • We may want to hide some business information. If I'm registering on your fresh website, and my user id is 5 - as a user I will instantly know I'm one of the early adopters and your business is not as established as it portrays itself!

Using encryption on-the-fly is a great alternative to using large primary keys (such as UUID) in your database - in many cases it's much better to spend CPU cycles of your application servers rather than storage, RAM, and CPU of the DB server.

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 worth to note that using a solution like this doesn't come without costs. It may take a bit of time getting used to non-scalar id in models. The additional decryption/encryption step for URLs may be annoying, so you have to decide if the advantages are worth the trouble in your case. 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 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.

About

Encrypted & authenticated integer id fields for Ecto schemas

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages