ExFPE: Format-preserving encryption for Elixir
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
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)