# Kryptex

<img width="100%" height="100%" alt="cryptex" src="https://github.com/user-attachments/assets/e5ec532e-69b6-4e3a-83ec-2417ca26c52b" />

<br />
<br />

`Kryptex` is an Elixir package for field-level encryption in Phoenix/Ecto apps.

- `AES-256-GCM` for authenticated encryption
- random IV per encryption call
- key id embedded in ciphertext for key rotation safety
- encryption/decryption performed through an Ecto custom type
- a **`Kryptex.Plug`** you can mount in a Phoenix endpoint (or router pipeline) so misconfigured keys fail at request time and the active encryption key id is available on `conn.assigns`

## Table of contents

- [Why this approach](#why-this-approach)
- [Install](#install)
- [Quickstart](#quickstart)
  - [Configure keys](#configure-keys)
- [Key rotation example](#key-rotation-example)
  - [Step 1: Add a new key and switch writes to it](#step-1-add-a-new-key-and-switch-writes-to-it)
  - [Step 2: Re-encrypt old rows in the background (optional but recommended)](#step-2-re-encrypt-old-rows-in-the-background-optional-but-recommended)
  - [Step 3: Retire old keys only after verification](#step-3-retire-old-keys-only-after-verification)
- [Configure your own model fields](#configure-your-own-model-fields)
- [Postgres support out of the box](#postgres-support-out-of-the-box)
- [Phoenix Plug (`Kryptex.Plug`)](#phoenix-plug-kryptexplug)
  - [Add the plug to a Phoenix app (endpoint)](#add-the-plug-to-a-phoenix-app-endpoint)
  - [Add the plug to a router pipeline (optional)](#add-the-plug-to-a-router-pipeline-optional)
  - [Reading the assign in plugs, controllers, or LiveView](#reading-the-assign-in-plugs-controllers-or-liveview)
- [Development](#development)
- [Security notes](#security-notes)
- [License](#license)

## Why this approach

In this package, encryption happens in custom Ecto type callbacks (`dump/load`), and builds on top of that using `Ecto.ParameterizedType` so each schema can configure encrypted fields directly.

## Install

Add dependency:

```elixir
defp deps do
  [
    {:kryptex, "~> 0.1.0"}
  ]
end
```

Then run:

```bash
mix deps.get
```

## Quickstart

This walkthrough encrypts `email`, `full_name`, and `metadata` on a `users` table. Complete [Install](#install) first, then follow the steps below.

### Configure keys

Add a data-encryption key (DEK) to your environment, then wire the keyring in `config/runtime.exs` (recommended):

```bash
# generate a 32-byte key (base64) and export it, e.g. in .env or your deploy secrets
export KRYPTEX_DEK_1="$(mix run -e 'IO.puts(:crypto.strong_rand_bytes(32) |> Base.encode64())')"
```

```elixir
config :kryptex,
  keys: [
    %{id: 1, key: System.fetch_env!("KRYPTEX_DEK_1")}
  ],
  default_key_id: 1
```

Keys can be either base64-encoded 32-byte values or raw 32-byte binaries. Generate one in an IEx session:

```elixir
:crypto.strong_rand_bytes(32) |> Base.encode64()
```

For multiple keys and rotation, see [Key rotation example](#key-rotation-example).

### Migration

Encrypted values are stored as `bytea` in PostgreSQL. Use `Kryptex.PostgresMigration` when creating the table:

```elixir
defmodule MyApp.Repo.Migrations.CreateUsers do
  use Ecto.Migration
  use Kryptex.PostgresMigration

  def change do
    create table(:users) do
      add_encrypted :email, null: false
      add_encrypted :full_name
      add_encrypted :metadata
      timestamps()
    end
  end
end
```

Run the migration:

```bash
mix ecto.migrate
```

### Schema

Declare encrypted fields on your schema with `Kryptex.Schema`:

```elixir
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  use Kryptex.Schema

  schema "users" do
    encrypted_field :email, :string
    encrypted_field :full_name, :string
    encrypted_field :metadata, :map
  end
end
```

At runtime, Ecto encrypts on `dump` and decrypts on `load`—your application code works with normal Elixir values:

```elixir
user =
  %MyApp.Accounts.User{}
  |> Ecto.Changeset.change(%{
    email: "alice@example.com",
    full_name: "Alice",
    metadata: %{"plan" => "pro"}
  })
  |> MyApp.Repo.insert!()

MyApp.Repo.get!(MyApp.Accounts.User, user.id).email
# => "alice@example.com"
```

### Phoenix plug (optional)

In a Phoenix app, mount `Kryptex.Plug` on the endpoint so misconfigured keys fail at request time and `conn.assigns.kryptex_key_id` reflects the active write key:

```elixir
# lib/my_app_web/endpoint.ex
plug Kryptex.Plug
```

See [Phoenix Plug (`Kryptex.Plug`)](#phoenix-plug-kryptexplug) for router pipelines and reading the assign.

## Key rotation example 

Kryptex writes new ciphertext with `default_key_id`, and reads old ciphertext
with the embedded `key_id` inside each stored payload.

That means rotation is usually a 3-step rollout:

### Step 1: Add a new key and switch writes to it

Current config:

```elixir
config :kryptex,
  keys: [
    %{id: 1, key: System.fetch_env!("KRYPTEX_DEK_1")},
    %{id: 2, key: System.fetch_env!("KRYPTEX_DEK_2")}
  ],
  default_key_id: 2
```

Rotate by adding key `3`, then switch the default:

```elixir
config :kryptex,
  keys: [
    %{id: 1, key: System.fetch_env!("KRYPTEX_DEK_1")},
    %{id: 2, key: System.fetch_env!("KRYPTEX_DEK_2")},
    %{id: 3, key: System.fetch_env!("KRYPTEX_DEK_3")}
  ],
  default_key_id: 3
```

After deploy:
- new inserts/updates are encrypted with key `3`
- existing rows encrypted with keys `1` and `2` still decrypt correctly

### Step 2: Re-encrypt old rows in the background (optional but recommended)

If you want all rows on the latest key, run a backfill job that:

1. loads records in batches,
2. rewrites encrypted fields with the same logical values,
3. persists records so Kryptex re-dumps with the new `default_key_id`.

Pseudo-flow:

```elixir
for user <- MyApp.Repo.stream(MyApp.Accounts.User) do
  user
  |> Ecto.Changeset.change(%{
    email: user.email,
    full_name: user.full_name,
    metadata: user.metadata
  })
  |> MyApp.Repo.update!()
end
```

This keeps plaintext unchanged while forcing re-encryption with key `3`.

### Step 3: Retire old keys only after verification

Before removing old keys:
- verify backfill completion
- verify no payloads remain for older key ids (from logs/DB sampling)
- verify reads in production for a safe window

Then remove the old key(s) from config:

```elixir
config :kryptex,
  keys: [
    %{id: 3, key: System.fetch_env!("KRYPTEX_DEK_3")}
  ],
  default_key_id: 3
```

Important: if any row still has `key_id` 1 or 2, removing those keys will make
those rows undecryptable.

## Configure your own model fields

Any schema can choose which fields are encrypted (see [Quickstart](#quickstart) for a full example).

```elixir
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  use Kryptex.Schema

  schema "users" do
    encrypted_field :email, :string
    encrypted_field :full_name, :string
    encrypted_field :metadata, :map
  end
end
```

You can also skip the macro and declare directly:

```elixir
field :email, Kryptex.EncryptedField, source_type: :string
```

## Postgres support out of the box

Encrypted data is stored as `:binary` in Ecto, which maps to `bytea` in PostgreSQL. The [Quickstart](#quickstart) migration example is the usual starting point; details below:

Migration example:

```elixir
defmodule MyApp.Repo.Migrations.CreateUsers do
  use Ecto.Migration
  use Kryptex.PostgresMigration

  def change do
    create table(:users) do
      add_encrypted :email, null: false
      add_encrypted :full_name
      add_encrypted :metadata
      timestamps()
    end
  end
end
```

## Phoenix Plug (`Kryptex.Plug`)

Field encryption is handled by **`Kryptex.EncryptedField`** in your Ecto schemas (see [Quickstart](#quickstart)).  
**`Kryptex.Plug`** is optional but useful in Phoenix: on each request it resolves the keyring (so missing or invalid `:kryptex` config surfaces immediately) and sets **`conn.assigns.kryptex_key_id`** to the id of the key used for **new** ciphertext (`default_key_id`).

It is a plain **Plug** (`Plug.Conn`), so it works anywhere in the Plug stack; in Phoenix the usual place is your **endpoint**, early in the plug chain.

### Add the plug to a Phoenix app (endpoint)

In `lib/my_app_web/endpoint.ex`, after the early instrumentation plugs and before parsers/session/router is a common choice:

```elixir
defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  # ... existing plugs, e.g. RequestId, Telemetry ...

  plug Kryptex.Plug

  # ... Plug.Parsers, Plug.Session, MyAppWeb.Router, etc. ...
end
```

Ensure **`config :kryptex, ...`** is loaded in `config/runtime.exs` (or equivalent) in all environments where the endpoint runs, so `Kryptex.Keyring` can read keys at runtime.

### Add the plug to a router pipeline (optional)

If you only want the check on certain scopes (e.g. browser or API), you can plug it in a **pipeline** in `lib/my_app_web/router.ex` instead of (or in addition to) the endpoint. Putting it on the endpoint is usually simpler so every request sees the same keyring state.

```elixir
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug Kryptex.Plug
    # ... fetch_session, protect_from_forgery, etc.
  end

  scope "/", MyAppWeb do
    pipe_through :browser
    # ...
  end
end
```

### Reading the assign in plugs, controllers, or LiveView

After the plug runs, **`conn.assigns.kryptex_key_id`** is the integer key id used as “current” for encryption (same as `Kryptex.Keyring.current_key_id/0`). You can use it for logging, debugging, or passing metadata to telemetry.

## Development

Run tests:

```bash
mix test
```

Run Credo:

```bash
mix credo --strict
```

Generate docs:

```bash
mix docs
```

## Security notes

- Keep keys outside source control (environment variables or KMS).
- Rotate keys by appending a new key id and switching `default_key_id`.
- Existing rows remain decryptable because key id is preserved in payload.
- The GCM additional authenticated data (AAD) string is versioned with the library name; if you change it in a fork, existing ciphertext will not decrypt until you re-encrypt.

## License

MIT
