ExFPE: Format-preserving encryption for Elixir

Copy Markdown View Source

Hex downloads License Elixir Versions Erlang Versions CI status

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. 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 API reference Last commit

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

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

The docs are published on HexDocs.

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.

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.

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.

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.

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

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

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

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.

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.

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

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

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

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

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

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

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.

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 managed by a process placed under your supervision tree. See ExFPE for details.

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