Packages

Encrypted integer id fields for Ecto schemas

Current section

Files

Jump to
ecto_encrypted_id lib ecto_encrypted_id.ex
Raw

lib/ecto_encrypted_id.ex

defmodule EctoEncryptedId do
@moduledoc """
Documentation for `EctoEncryptedId`.
"""
@doc false
def app_env_key() do
Application.fetch_env!(:ecto_encrypted_id, :settings)[:secret_key]
end
@doc """
Helper function to delegate to Phoenix.Param implementations for Ecto schemas.
Returns encrypted version of the id.
## Example
In your schema module:
defimpl Phoenix.Param, for: __MODULE__ do
defdelegate to_param(term), to: EctoEncryptedId, as: :encrypted_param
end
"""
def encrypted_param(%{id: %{encrypted: id}}), do: id
@doc """
Helper function to delegate to Phoenix.Param implementations for Ecto schemas.
Returns plain version of the id.
## Example
In your schema module:
defimpl Phoenix.Param, for: __MODULE__ do
defdelegate to_param(term), to: EctoEncryptedId, as: :plain_param
end
"""
def plain_param(%{id: %{plain: id}}), do: id
defmodule DecryptionError do
@moduledoc """
Exception raised when bang `from_encrypted!/2` functions can't decrypt a value
"""
defexception [:message, plug_status: 400]
end
defmacro __using__(opts) do
iv_salt = Keyword.fetch!(opts, :salt)
secret_key_fn = Keyword.get(opts, :secret_key_fn, &EctoEncryptedId.app_env_key/0)
quote location: :keep do
use Ecto.Type
defmodule Id do
@moduledoc """
Container type for the encrypted field.
Implements struct storing the id and related functions.
"""
alias EctoEncryptedId.Encryption
@type t :: %__MODULE__{
encrypted: String.t(),
plain: integer()
}
@enforce_keys [:encrypted, :plain]
defstruct [:encrypted, :plain]
@doc """
Create id struct from an encrypted value.
Supported options:
- :secret_key_fn - custom function to fetch secret key
Returns `{:ok, %Id{}}` on success or :error on failure
"""
@spec from_encrypted(id :: String.t(), opts :: keyword()) :: {:ok, t()} | :error
def from_encrypted(id, opts \\ []) do
key = Keyword.get(opts, :secret_key_fn, unquote(secret_key_fn)).()
case Encryption.decrypt(id, key) do
{:ok, decrypted} -> {:ok, %__MODULE__{encrypted: id, plain: decrypted}}
:error -> :error
end
end
@doc """
Create id struct from an encrypted value.
When `key` argument is not provided use the secret key function
(default function gets the key from the application environment).
Supported options:
- :secret_key_fn - custom function to fetch secret key
Returns `%Id{}` on success.
Raises `EctoEncryptedId.DecryptionError` on failure.
"""
@spec from_encrypted!(id :: String.t(), opts :: keyword()) :: t()
def from_encrypted!(id, opts \\ []) do
key = Keyword.get(opts, :secret_key_fn, unquote(secret_key_fn)).()
case Encryption.decrypt(id, key) do
{:ok, decrypted} -> %__MODULE__{encrypted: id, plain: decrypted}
:error -> raise(DecryptionError, "Invalid encrypted id: " <> id)
end
end
@doc """
Create id struct from a plain integer value.
Supported options:
- :secret_key_fn - custom function to fetch secret key
Returns %Id{}.
"""
@spec from_plain(id :: integer(), opts :: keyword()) :: t()
def from_plain(id, opts \\ []) do
key = Keyword.get(opts, :secret_key_fn, unquote(secret_key_fn)).()
%__MODULE__{
encrypted: Encryption.encrypt(id, key, unquote(iv_salt)),
plain: id
}
end
end
# To avoid leaking plain ids in HTML
if Code.ensure_loaded?(Phoenix.HTML.Safe) do
defimpl Phoenix.HTML.Safe, for: Id do
def to_iodata(id), do: id.enc
end
end
# Ecto callbacks
def type, do: :id
def cast(id) when is_integer(id), do: {:ok, Id.from_plain(id)}
def cast(id) when is_binary(id), do: Id.from_encrypted(id)
def cast(%Id{} = id), do: {:ok, id}
def cast(_), do: :error
def dump(%Id{plain: plain}) when is_integer(plain), do: {:ok, plain}
def dump(_), do: :error
def load(id) when is_integer(id), do: {:ok, Id.from_plain(id)}
end
end
end