ExFPE behaviour (ex_fpe v0.1.0)

Copy Markdown View Source

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. See ExFPE.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).

t()

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

alphabet()

@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.

constraints()

@type constraints() :: %{min_length: pos_integer(), max_length: pos_integer()}

A mode's inclusive bounds on input length, as returned by constraints/1.

key()

@type key() :: ExFPE.FFX.key()

An AES key: 16, 24, or 32 bytes (AES-128/192/256).

mode()

@type mode() :: :ff1 | :ff3_1

A supported FPE mode.

radix()

@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).

t()

@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.

tweak()

@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

child_spec()

@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

__using__(list)

(macro)

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/3 builder and a start_link/3, backed by a uniquely named process holding the context in a :persistent_term;
  • encrypt/2, encrypt!/2, decrypt/2, decrypt!/2 that retrieve the context transparently; plus constraints/0 and ex_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")

constraints(ctx)

@spec constraints(t()) :: constraints()

Returns a ctx's mode-specific length constraints (min_length/max_length).

decrypt(ctx, tweak, ciphertext)

@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.

decrypt!(ctx, tweak, ciphertext)

@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.

encrypt(ctx, tweak, plaintext)

@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.

encrypt!(ctx, tweak, plaintext)

@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.

new(key, mode \\ :ff1, radix_or_alphabet)

@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.

new!(key, mode \\ :ff1, radix_or_alphabet)

@spec new!(key(), mode(), radix() | alphabet() | {:raw_only, radix()}) :: ctx :: t()

Like new/3, but returns the context directly and raises ExFPE.ArgumentError on failure.

raw_decrypt(ctx, tweak, cipherval, length)

@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.

raw_decrypt!(ctx, tweak, cipherval, length)

@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.

raw_encrypt(ctx, tweak, plainval, length)

@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.

raw_encrypt!(ctx, tweak, plainval, length)

@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.