Format-preserving encryption (FPE) for Elixir.
ExFPE encrypts a numerical string into another of the same length over the same alphabet, which 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.
To pick a mode explicitly, pass it as the second argument to new/3:
:ff1— the FF1 mode (default; variable-length tweak).:ff3_1— the FF3-1 mode (fixed 7-byte tweak). ⚠️ NIST removed FF3-1; use it only for interop with existing data. SeeExFPE.FF3_1.
Mode-specific rules
The tweak size and the length constraints on inputs depend on the mode.
- FF1 accepts a variable-length tweak (it may even be empty); see
ExFPE.FF1. - FF3-1 uses a fixed 7-byte (56-bit) tweak; see
ExFPE.FF3_1.
Guide
See the usage guide for worked, runnable examples — contexts,
built-in and custom alphabets, tweaks, alphabet-free raw integers, choosing a
mode, and the use ExFPE convenience for supervised contexts.
Summary
Types
The ordered symbols of a custom alphabet, given as a string.
A mode's inclusive bounds on input length, as returned by constraints/1.
An AES key: 16, 24, or 32 bytes (AES-128/192/256).
A supported FPE mode.
The base the numerical strings are written in, from 2 up to 65536
(or 65535 in the case of ExFPE.FF3_1).
An encryption/decryption context.
A tweak: public data that varies the ciphertext for a given key and plaintext.
Callbacks
Returns the Supervisor child spec for the module that use ExFPE.
Functions
Places an ExFPE context under your supervision tree so that you can encrypt
and decrypt without threading the context through every call.
Returns a ctx's mode-specific length constraints (min_length/max_length).
Decrypts ciphertext back into its plaintext numerical string, using tweak.
Like decrypt/3, but returns the plaintext directly and raises
ExFPE.InputError on failure.
Encrypts plaintext into a numerical string of the same length over the same
alphabet, using tweak.
Like encrypt/3, but returns the ciphertext directly and raises
ExFPE.InputError on failure.
Creates a context for both encryption and decryption from a key, an optional
mode (:ff1 by default), and either a radix, an alphabet, or
{:raw_only, radix}.
Like new/3, but returns the context directly and raises ExFPE.ArgumentError
on failure.
Decrypts the integer cipherval back into its plaintext value, skipping the
symbol alphabet.
Like raw_decrypt/4, but returns the plaintext value directly and raises
ExFPE.InputError on failure.
Encrypts the integer plainval directly, skipping the symbol alphabet.
Like raw_encrypt/4, but returns the ciphertext value directly and raises
ExFPE.InputError on failure.
Types
@type alphabet() :: String.t()
The ordered symbols of a custom alphabet, given as a string.
Each symbol is a single Unicode codepoint, and the radix is the amount of symbols (not graphemes).
For any alphabet not covered by ExFPE.Codec.Builtin, see
ExFPE.Codec.Custom for the exact rules.
@type constraints() :: %{min_length: pos_integer(), max_length: pos_integer()}
A mode's inclusive bounds on input length, as returned by constraints/1.
@type key() :: ExFPE.FFX.key()
An AES key: 16, 24, or 32 bytes (AES-128/192/256).
@type mode() :: :ff1 | :ff3_1
A supported FPE mode.
@type radix() :: ExFPE.FFX.radix()
The base the numerical strings are written in, from 2 up to 65536
(or 65535 in the case of ExFPE.FF3_1).
@opaque t()
An encryption/decryption context.
Built by new/3 (or new!/3) and passed to encrypt!/3 and decrypt!/3.
Bundles the chosen mode's algorithm (which holds the key) with how inputs map
to integers — an alphabet, or the raw integers of a {:raw_only, radix} context.
@type tweak() :: binary()
A tweak: public data that varies the ciphertext for a given key and plaintext.
Its length depends on the mode — variable for FF1, a fixed 7 bytes for FF3-1.
Callbacks
@callback child_spec() :: Supervisor.child_spec()
Returns the Supervisor child spec for the module that use ExFPE.
Implement it by calling the generated child_spec/2 or child_spec/3 with
your key, mode, and radix/alphabet (or {:raw_only, radix}) — see the moduledoc.
Functions
Places an ExFPE context under your supervision tree so that you can encrypt
and decrypt without threading the context through every call.
A module that declares use ExFPE gets:
- a
child_spec/2/child_spec/3builder and astart_link/3, backed by a uniquely named process holding the context in a:persistent_term; encrypt/2,encrypt!/2,decrypt/2,decrypt!/2that retrieve the context transparently; plusconstraints/0andex_fpe!/0.
You implement the child_spec/0 callback declaring your configuration, and
add MyModule.child_spec() to your supervision tree.
defmodule MyApp.CardCipher do
use ExFPE
@impl true
def child_spec do
child_spec(fetch_key(), :ff3_1, _radix = 10)
end
defp fetch_key, do: Application.fetch_env!(:my_app, :fpe_key)
end
# in your application's supervision tree:
children = [
MyApp.CardCipher.child_spec(),
# ...
]
# then, anywhere:
MyApp.CardCipher.encrypt!(tweak, "34436524")
@spec constraints(t()) :: constraints()
Returns a ctx's mode-specific length constraints (min_length/max_length).
@spec decrypt(t(), tweak(), ciphertext) :: {:ok, plaintext} | {:error, term()} when ciphertext: String.t(), plaintext: String.t()
Decrypts ciphertext back into its plaintext numerical string, using tweak.
Returns {:error, reason} if the tweak or input is invalid.
@spec decrypt!(t(), tweak(), ciphertext) :: plaintext when ciphertext: String.t(), plaintext: String.t()
Like decrypt/3, but returns the plaintext directly and raises
ExFPE.InputError on failure.
@spec encrypt(t(), tweak(), plaintext) :: {:ok, ciphertext} | {:error, term()} when plaintext: String.t(), ciphertext: String.t()
Encrypts plaintext into a numerical string of the same length over the same
alphabet, using tweak.
Returns {:error, reason} if the tweak or input is invalid.
@spec encrypt!(t(), tweak(), plaintext) :: ciphertext when plaintext: String.t(), ciphertext: String.t()
Like encrypt/3, but returns the ciphertext directly and raises
ExFPE.InputError on failure.
@spec new(key(), mode(), radix() | alphabet() | {:raw_only, radix()}) :: {:ok, ctx :: t()} | {:error, term()}
Creates a context for both encryption and decryption from a key, an optional
mode (:ff1 by default), and either a radix, an alphabet, or
{:raw_only, radix}.
A radix or alphabet encrypts strings with encrypt/3/decrypt/3;
{:raw_only, radix} skips symbols entirely and works with integers through
raw_encrypt/4/raw_decrypt/4.
Returns {:error, reason} if any argument is invalid.
Like new/3, but returns the context directly and raises ExFPE.ArgumentError
on failure.
@spec raw_decrypt(t(), tweak(), cipherval, length) :: {:ok, plainval} | {:error, term()} when cipherval: non_neg_integer(), length: pos_integer(), plainval: non_neg_integer()
Decrypts the integer cipherval back into its plaintext value, skipping the
symbol alphabet.
The alphabet-free counterpart to decrypt/3; see raw_encrypt/4 for how
length and the 0 <= value < radix ** length bound work.
Returns {:error, reason} if the tweak or input is invalid.
@spec raw_decrypt!(t(), tweak(), cipherval, length) :: plainval when cipherval: non_neg_integer(), length: pos_integer(), plainval: non_neg_integer()
Like raw_decrypt/4, but returns the plaintext value directly and raises
ExFPE.InputError on failure.
@spec raw_encrypt(t(), tweak(), plainval, length) :: {:ok, cipherval} | {:error, term()} when plainval: non_neg_integer(), length: pos_integer(), cipherval: non_neg_integer()
Encrypts the integer plainval directly, skipping the symbol alphabet.
This is the alphabet-free counterpart to encrypt/3: you hand it an integer
and it hands an integer back, leaving the mapping between integers and
whatever symbols they stand for up to you. Reach for it when your symbols
aren't a single Unicode scalar each (so a custom alphabet can't accept them),
when the value already lives as an integer in your system, or to avoid string
encoding on a hot path.
plainval is interpreted as length digits in the context's radix, most
significant first. length is significant and must be passed explicitly:
FPE treats leading zeroes as real symbols, so {value: 42, length: 2} ("42")
and {value: 42, length: 5} ("00042") encrypt differently and can't be told
apart from the value alone. The ciphertext preserves length.
plainval must fit its declared length — that is,
0 <= plainval < radix ** length — otherwise this returns
{:error, {:negative_value, plainval}} or
{:error, {:value_is_larger_than_declared_length}}.
Any context supports this, including one built from an alphabet: it operates on
the context's radix and ignores the alphabet. A context built with
{:raw_only, radix} supports only these functions, not encrypt/3.
Returns {:error, reason} if the tweak or input is invalid.
@spec raw_encrypt!(t(), tweak(), plainval, length) :: cipherval when plainval: non_neg_integer(), length: pos_integer(), cipherval: non_neg_integer()
Like raw_encrypt/4, but returns the ciphertext value directly and raises
ExFPE.InputError on failure.