Skip to main content

README.md

# ExFPE: Format-preserving encryption for Elixir

[![Hex downloads](https://img.shields.io/hexpm/dt/ex_fpe.svg)](https://hex.pm/packages/ex_fpe)
[![License](https://img.shields.io/hexpm/l/ex_fpe.svg)](https://github.com/g-andrade/ex_fpe/blob/main/LICENSE)
[![Elixir Versions](https://img.shields.io/badge/Elixir-1.14%20to%201.20-blue)](https://elixir-lang.org/)
[![Erlang Versions](https://img.shields.io/badge/Erlang%2FOTP-26%20to%2028-blue)](https://www.erlang.org)
[![CI status](https://github.com/g-andrade/ex_fpe/actions/workflows/ci.yml/badge.svg)](https://github.com/g-andrade/ex_fpe/actions/workflows/ci.yml)

ExFPE encrypts a numerical string into another of the **same length over the
same alphabet**. This is useful to e.g. store an encrypted credit card number
in a field that only accepts credit-card-shaped values, and other suchlike
applications.

By default it uses **FF1** (`ExFPE.FF1`), the only mode approved by NIST in [SP
800-38Gr1 2pd](https://csrc.nist.gov/pubs/sp/800/38/g/r1/2pd). The examples
below all use the default. The other mode is FF3-1 (`ExFPE.FF3_1`), which NIST
no longer recommends; reach for it only to interoperate with data that was
already encrypted with FF3-1.

## Installation

[![Latest version](https://img.shields.io/hexpm/v/ex_fpe.svg?style=flat)](https://hex.pm/packages/ex_fpe)
[![API reference](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ex_fpe/)
[![Last commit](https://img.shields.io/github/last-commit/g-andrade/ex_fpe.svg)](https://github.com/g-andrade/ex_fpe/commits/main)

Add `ex_fpe` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:ex_fpe, "~> 0.1.0"}
  ]
end
```

The docs are published on [HexDocs](https://hexdocs.pm/ex_fpe).

## Usage

### Context

We start by creating a context with `new!/2`, passing it a cryptographic key
and a radix. With no mode given, the default (FF1) is used.

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> _ctx = ExFPE.new!(key, _radix = 10)
```

Keys can be:
* 32 bytes long for AES-256
* 24 bytes long for AES-192
* 16 bytes long for AES-128

Radix is an integer between 2 and 36. For larger radixes up to 65535, you'll
need either a custom alphabet or the alphabet-free raw mode - more on both
later.

### Encryption and decryption

We're going to `encrypt!/3` our `plaintext` numerical string, in base 10,
and get another of equal length, `ciphertext`, which we can `decrypt!/3`
to get the `plaintext` back.

A `tweak` is required; explanation further below. Its size depends on the mode:
FF1 (the default) accepts a variable-length byte string, so the 7-byte tweak
below is just one valid choice.

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, _radix = 10)
iex> tweak = "dev.env"
iex> plaintext = "34436524"
iex> ciphertext = ExFPE.encrypt!(ctx, tweak, plaintext)
iex> ^plaintext = ExFPE.decrypt!(ctx, tweak, ciphertext)
```

### Leading zeroes matter

⚠️ Keep in mind that **leading zeroes are significant**. Ciphertexts are always
of equal length to their respective plaintexts, and vice-versa.

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, _radix = 10)
iex> tweak = "dev.env"
iex> plaintext1 =   "34436524"
iex> plaintext2 = "0034436524"
iex> ciphertext1 = ExFPE.encrypt!(ctx, tweak, plaintext1)
iex> ciphertext2 = ExFPE.encrypt!(ctx, tweak, plaintext2)
iex> false = (ciphertext2 == ciphertext1)
iex> true = (String.length(ciphertext1) == String.length(plaintext1))
iex> true = (String.length(ciphertext2) == String.length(plaintext2))
```

### Tweaks

Tweaks may be public information used to produce different ciphertexts for
the same plaintext.

**They are important in FPE modes**, since the number of possible strings may
be somewhat small. In such a scenario, the tweak should vary with each instance
of the encryption whenever possible.

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, _radix = 10)
iex> plaintext= "135522432"
iex> tweak1 = "dev.env"
iex> tweak2 = "prod.env"
iex> ciphertext1 = ExFPE.encrypt!(ctx, tweak1, plaintext)
iex> ciphertext2 = ExFPE.encrypt!(ctx, tweak2, plaintext)
iex> ciphertext2 != ciphertext1
```

### Built-in alphabet

For radix values between 2 and 36, if what `Integer.to_string/2` produces is
good enough, you only need to specify the `radix` when building your `ctx`.

Both `plaintext` and `ciphertext` will be encoded in the chosen base.

#### Base 8

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, _radix = 8)
iex> tweak = "staging.env"
iex> plaintext = "34436524"
iex> ciphertext = ExFPE.encrypt!(ctx, tweak, plaintext)
iex> ^plaintext = ExFPE.decrypt!(ctx, tweak, ciphertext)
```

#### Base 16

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, _radix = 16)
iex> tweak = "test.env"
iex> plaintext = "AFD093902C"
iex> ciphertext = ExFPE.encrypt!(ctx, tweak, plaintext)
iex> ^plaintext = ExFPE.decrypt!(ctx, tweak, ciphertext)
```

#### Base 36

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, _radix = 36)
iex> tweak = "main-deploy"
iex> plaintext = "ZZZAFD093902CBZDE"
iex> ciphertext = ExFPE.encrypt!(ctx, tweak, plaintext)
iex> ^plaintext = ExFPE.decrypt!(ctx, tweak, ciphertext)
```

#### Case insensitivity to input

Even though the output of either `encrypt!/3` or `decrypt!/3` is
upper case, `ExFPE.Codec.Builtin` accepts inputs in any case.

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> radix = 16
iex> ctx = ExFPE.new!(key, radix)
iex> tweak = "tweak23"
iex> input = "aBcDDFF01234eeEee"
iex> _ciphertext = ExFPE.encrypt!(ctx, tweak, input)
iex> _plaintext = ExFPE.decrypt!(ctx, tweak, input)
```

#### Lower case output

If you want to use `ExFPE.Codec.Builtin` but desire lower case outputs, you can
do it by declaring the alphabet when creating `ctx`.

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> alphabet = "0123456789abcdef" # radix 16
iex> ctx = ExFPE.new!(key, alphabet)
iex> tweak = "dev.env"
iex> input = "aBcDDFF01234eeEee"
iex> ciphertext = ExFPE.encrypt!(ctx, tweak, input)
iex> plaintext = ExFPE.decrypt!(ctx, tweak, input)
iex> ^ciphertext = String.downcase(ciphertext)
iex> ^plaintext = String.downcase(plaintext)
```

### Custom alphabets

Whether you need a radix larger than 36, or use symbols other than 0-9, A-Z
in your numerical strings (or use such symbols in a different order), custom
alphabets are supported.

Note that custom alphabets are norm insensitive but **case sensitive**.
The reasoning behind this can be found under `ExFPE.Codec.Custom`.

Each symbol must be a single Unicode codepoint that stands on its own as one
visual unit; alphabets are validated at construction. See
`ExFPE.Codec.Custom` for the exact rules and the guarantees they buy.

#### Base 20 with custom alphabet

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> alphabet = "abcdefghij0123456789"
iex> ctx = ExFPE.new!(key, alphabet)
iex> tweak = "testing"
iex> plaintext = "34534abcd32235"
iex> ciphertext = ExFPE.encrypt!(ctx, tweak, plaintext)
iex> ^plaintext = ExFPE.decrypt!(ctx, tweak, ciphertext)
```

#### Base 40 with custom alphabet

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> alphabet = "0123456789abcdefghijklmnopqrstuvwxyz@#/*"
iex> ctx = ExFPE.new!(key, alphabet)
iex> tweak = "testing"
iex> plaintext = "34534ab@@@@@/cd32235"
iex> ciphertext = ExFPE.encrypt!(ctx, tweak, plaintext)
iex> ^plaintext = ExFPE.decrypt!(ctx, tweak, ciphertext)
```

#### Unicode support

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> alphabet = "🌕🌖🌗🌘🌑🌒🌓🌔"
iex> ctx = ExFPE.new!(key, alphabet)
iex> tweak = "example"
iex> plaintext = "🌖🌕🌘🌑🌓🌗🌔🌒🌒🌒🌒"
iex> ciphertext = ExFPE.encrypt!(ctx, tweak, plaintext)
iex> ^plaintext = ExFPE.decrypt!(ctx, tweak, ciphertext)
```

### No alphabet

If you wish to handle translation of integers into and from symbols yourself,
build the context with `{:raw_only, radix}` and use `ExFPE.raw_encrypt!/4` and
`ExFPE.raw_decrypt!/4`. They receive, and return, an integer value; you pass
its length (symbol count) separately, because **leading zeroes are
significant** in FPE and can't be recovered from the value alone.

Encryption and decryption act on the value as if it were encoded in that radix,
most significant symbol first.

#### Radix 10

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, {:raw_only, _radix = 10})
iex> tweak = "example"
iex> plainval = 1234567
iex> length = 10
iex>
iex> cipherval = ExFPE.raw_encrypt!(ctx, tweak, plainval, length)
iex> ^plainval = ExFPE.raw_decrypt!(ctx, tweak, cipherval, length)
```

#### Radix 500

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, {:raw_only, _radix = 500})
iex> tweak = "foobar"
iex> plainval = 1234567
iex> length = 10
iex>
iex> cipherval = ExFPE.raw_encrypt!(ctx, tweak, plainval, length)
iex> ^plainval = ExFPE.raw_decrypt!(ctx, tweak, cipherval, length)
```

#### Radix 65535

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, {:raw_only, _radix = 65535})
iex> tweak = "tweak55"
iex> plainval = 1234567
iex> length = 10
iex>
iex> cipherval = ExFPE.raw_encrypt!(ctx, tweak, plainval, length)
iex> ^plainval = ExFPE.raw_decrypt!(ctx, tweak, cipherval, length)
```

### Choosing a mode

Everything above uses the default mode, FF1. To select a mode explicitly,
pass it as the second argument to `new!/3`. The only other mode is FF3-1,
which is **no longer NIST-approved** (see `ExFPE.FF3_1`) — reach for it only to
interoperate with data that was already encrypted with FF3-1. It takes a
fixed 7-byte tweak.

```elixir
iex> key = :crypto.strong_rand_bytes(32)
iex> ctx = ExFPE.new!(key, :ff3_1, _radix = 10)
iex> tweak = "dev.env"
iex> plaintext = "34436524"
iex> ciphertext = ExFPE.encrypt!(ctx, tweak, plaintext)
iex> ^plaintext = ExFPE.decrypt!(ctx, tweak, ciphertext)
```

## Convenience: `use ExFPE`

Having to thread a `ctx` through every `encrypt!/3` and `decrypt!/3` call can be
cumbersome. If you'd rather not, `use ExFPE` generates functions that retrieve
the context transparently, storing it in a uniquely named
[`persistent_term`](https://www.erlang.org/doc/man/persistent_term) managed by a
process placed under your supervision tree. See `ExFPE` for details.

```elixir
iex> defmodule MyApp.CardCipher do
iex>   use ExFPE
iex>
iex>   @impl true
iex>   def child_spec() do
iex>     child_spec(fetch_key(), _radix = 10)
iex>   end
iex>
iex>   defp fetch_key(), do: Application.fetch_env!(:my_app, :fpe_key)
iex> end
iex>
iex>
iex> defmodule MyApp.Application do
iex>   def start(_type, _args) do
iex>      children = [
iex>        MyApp.CardCipher.child_spec(),
iex>      ]
iex>
iex>      opts = [strategy: :one_for_one, name: MyApp.Supervisor]
iex>      Supervisor.start_link(children, opts)
iex>   end
iex> end
iex>
iex>
iex> Application.put_env(:my_app, :fpe_key, :crypto.strong_rand_bytes(32))
iex> {:ok, _} = MyApp.Application.start(:normal, [])
iex>
iex>
iex> tweak = "test.env"
iex> plaintext = "34436524"
iex> ciphertext = MyApp.CardCipher.encrypt!(tweak, plaintext)
iex> ^plaintext = MyApp.CardCipher.decrypt!(tweak, ciphertext)
```

## License

[MIT](LICENSE)