cloak v0.7.0 Cloak.Vault behaviour View Source

Encrypts and decrypts data, using a configured cipher.

Configuration

When used, the vault expects the :otp_app option. The :otp_app option should point to an OTP application that has the vault configuration.

For example, the vault:

defmodule MyApp.Vault do
  use Cloak.Vault, otp_app: :my_app
end

Could be configured with:

config :my_app, MyApp.Vault,
  json_library: Poison,
  ciphers: [
    default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: <<...>>}
  ]

The configuration options are:

:json_library

Used to convert data types like lists and maps into binary so that they can be encrypted. (Default: Poison)

:ciphers

A Keyword list of Cloak.Cipher modules to use for encryption or decryption, in the following format:

{label, {cipher_module, opts}}

The opts are specific to each cipher module. Check their documentation for details. The following ciphers ship with Cloak:

IMPORTANT: THE FIRST CONFIGURED CIPHER IN THE LIST IS THE DEFAULT FOR ENCRYPTING ALL NEW DATA. (Regardless of its label!) The other ciphers are, by default, used only for decryption. (This behavior can be overriden on a field-by-field basis, see below)

Runtime Configuration

Vaults can be configured at runtime using the init/1 callback. This allows you to easily fetch values like environment variables in a reliable way.

The configuration from the :otp_app is passed as the first argument to the callback, allowing you to append to or change it at will.

defmodule MyApp.Vault do
  use Cloak.Vault, otp_app: :my_app

  @impl Cloak.Vault
  def init(config) do
    config =
      Keyword.put(config, :ciphers, [
        default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: System.get_env("CLOAK_KEY")}
      ])

    {:ok, config}
  end
end

Configuring Ecto Types

Once you have a configured vault, you can define Ecto.Type modules which use it for encryption/decryption.

defmodule MyApp.Encrypted.Binary do
  use Cloak.Fields.Binary, vault: MyApp.Vault
end

You can also specify that a field uses a particular labeled cipher from your configuration:

defmodule MyApp.Encrypted.Binary do
  use Cloak.Fields.Binary,
    vault: MyApp.Vault,
    cipher: :custom # corresponds to the `label` of the cipher
end

The field will only use the specified cipher for encryption, not decryption. It will decrypt stored data with whichever cipher originally generated it.

The following Cloak field types are available:

Elixir TypeEcto TypeCloak Type
String:string / :binaryCloak.Fields.Binary
Date:dateCloak.Fields.Date
DateTime:utc_datetimeCloak.Fields.DateTime
Float:floatCloak.Fields.Float
Integer:integerCloak.Fields.Integer
Map:mapCloak.Fields.Map
NaiveDateTime:naive_datetimeCloak.Fields.NaiveDateTime
Time:timeCloak.Fields.Time
[Integer]{:array, :integer}Cloak.Fields.IntegerList
[String]{:array, :string}Cloak.Fields.StringList

Usage

Direct Usage

You can use the vault directly by calling its functions.

MyApp.Vault.encrypt("plaintext")
# => {:ok, <<...>>}

MyApp.Vault.decrypt(ciphertext)
# => {:ok, "plaintext"}

See the documented callbacks below for the functions you can call.

With Schemas

Once you have configured your types, you can use them in your Ecto.Schemas. Be sure to first create the fields with the :binary type in your migration:

# in your migration
create table(:users) do
  add :email, :binary
end

Then, use the custom Ecto.Type you defined, as in this example:

defmodule MyApp.Accounts.User do
  use Ecto.Schema

  import Ecto.Changeset

  schema "users" do
    field :email, MyApp.Encrypted.Binary
  end

  def changeset(struct, attrs \\ %{}) do
    struct
    |> cast(attrs, [:email])
  end
end

In this case, the :email field will now be transparently encrypted when written to the database and decrypted when loaded out of the database.

Querying Encrypted Data

By design, Cloak ciphers produce unique ciphertext each time, even if the value remains the same. As a result, you cannot query on an encrypted schema field directly.

However, you can create a mirror of a encrypted field which contains a predictable hashed value. This allows you to query for exact matches.

In your migration, create a [field_name]_hash field:

alter table(:users) do
  add :email_hash, :binary
end

Then, in your schema, use one of Cloak’s provided hash types, which are:

TypeEcto TypeField
String:string / :binaryCloak.Fields.SHA256

In this example, we’ll use Cloak.Fields.SHA256:

schema "users" do
  field :email, MyApp.Encrypted.Binary
  field :email_hash, Cloak.Fields.SHA256
end

Finally, in your changeset/2 function, ensure that the _hash field is updated every time the main field is changed:

def changeset(struct, attrs \\ %{}) do
  struct
  |> cast(attrs, [:email])
  |> put_hashed_fields()
end

defp put_hashed_fields(changeset) do
  changeset
  |> put_change(:email_hash, get_field(changeset, :email))
end

Now, you can query by the _hash field anywhere you might have previously queried by the main field.

Repo.get_by(MyApp.Accounts.User, email_hash: "test@example.com")
# => %MyApp.Accounts.User{
#      email: "test@example.com",
#      email_hash:
#        <<151, 61, 254, 70, 62, 200, 87, 133, 245, 249, 90, 245, 186, 57,
#        6, 238, 219, 45, 147, 28, 36, 230, 152, 36, 168, 158, 166, 93,
#        186, 78, 129, 59>>
#    }

Rotating Keys

See Mix.Tasks.Cloak.Migrate for instructions on how to rotate keys.

Link to this section Summary

Callbacks

Like decrypt/1, but raises any errors

Decrypts a binary with the configured cipher that generated the binary. Automatically detects which cipher to use, based on the ciphertext

Like encrypt/1, but raises any errors

Like encrypt/2, but raises any errors

Encrypts a binary using the first configured cipher in the vault’s configured :ciphers list

Encrypts a binary using the vault’s configured cipher with the corresponding label

Accepts configuration from the vault’s :otp_app, and returns updated configuration. Useful for changing configuration based on the runtime environment

The JSON library the vault uses to convert maps and lists into JSON binaries before encryption

Link to this section Types

Link to this type ciphertext() View Source
ciphertext() :: binary()
Link to this type plaintext() View Source
plaintext() :: binary()

Link to this section Callbacks

Link to this callback decrypt!(ciphertext) View Source
decrypt!(ciphertext()) :: plaintext() | no_return()

Like decrypt/1, but raises any errors.

Link to this callback decrypt(ciphertext) View Source
decrypt(ciphertext()) :: {:ok, String.t()} | {:error, Exception.t()}

Decrypts a binary with the configured cipher that generated the binary. Automatically detects which cipher to use, based on the ciphertext.

Link to this callback encrypt!(plaintext) View Source
encrypt!(plaintext()) :: ciphertext() | no_return()

Like encrypt/1, but raises any errors.

Link to this callback encrypt!(plaintext, label) View Source
encrypt!(plaintext(), label()) :: ciphertext() | no_return()

Like encrypt/2, but raises any errors.

Link to this callback encrypt(plaintext) View Source
encrypt(plaintext()) :: {:ok, ciphertext()} | {:error, Exception.t()}

Encrypts a binary using the first configured cipher in the vault’s configured :ciphers list.

Link to this callback encrypt(plaintext, label) View Source
encrypt(plaintext(), label()) :: {:ok, ciphertext()} | {:error, Exception.t()}

Encrypts a binary using the vault’s configured cipher with the corresponding label.

Link to this callback init(config) View Source
init(config :: Keyword.t()) :: {:ok, Keyword.t()} | :error

Accepts configuration from the vault’s :otp_app, and returns updated configuration. Useful for changing configuration based on the runtime environment.

Example

def init(config) do
  config =
    Keyword.put(config, :ciphers, [
      default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: System.get_env("CLOAK_KEY")}
    ])

  {:ok, config}
end
Link to this callback json_library() View Source
json_library() :: module()

The JSON library the vault uses to convert maps and lists into JSON binaries before encryption.