Powered by AppSignal & Oban Pro

Advanced ExkPasswd configuration

notebooks/advanced.livemd

Advanced ExkPasswd configuration

Mix.install([
  {:exk_passwd, "~> 0.2.0"}
])

Case and length

for mode <- [:none, :lower, :upper, :capitalize, :alternate, :random, :invert] do
  config = ExkPasswd.Config.new!(num_words: 3, word_length: 5..6, case_transform: mode, separator: "-")
  {mode, ExkPasswd.generate(config)}
end

word_length is an ascending range. Non-Latin dictionaries can opt out of the default 4–10 range by declaring explicit bounds:

ExkPasswd.Config.new!(word_length: 2..4, word_length_bounds: 1..10)

Padding is a minimum, never a truncation rule

config =
  ExkPasswd.Config.new!(
    num_words: 2,
    digits: {0, 0},
    separator: "-",
    padding: %{char: "=", before: 0, after: 0, to_length: 32}
  )

password = ExkPasswd.generate(config)
{password, String.length(password)}

If the natural password is already longer than 32 characters, it is returned unchanged. Choose shorter words or fewer components for a destination with a hard maximum.

Top-level substitutions

config =
  ExkPasswd.Config.new!(
    num_words: 3,
    case_transform: :lower,
    substitutions: %{"a" => "@", "e" => "3", "i" => "1", "o" => "0"},
    substitution_mode: :always
  )

ExkPasswd.generate(config)

Use :random to choose the original or substituted form independently per word. The entropy model credits that choice only when the forms differ.

Custom dictionaries

words = ["café", "naïve", "résumé", "zürich", "façade"]
ExkPasswd.Dictionary.load_custom(:accented, words)

config =
  ExkPasswd.Config.new!(
    dictionary: :accented,
    word_length: 4..6,
    num_words: 3,
    case_transform: :lower,
    separator: "."
  )

ExkPasswd.generate(config)

Custom words are normalized to NFC and must be unique afterward. Loading a dictionary writes to :persistent_term, so do it at application startup rather than for each request.

Protocol transforms

defmodule BracketTransform do
  defstruct []

  defimpl ExkPasswd.Transform do
    def apply(_, word, _config), do: "[" <> word <> "]"
    def entropy_bits(_, _config), do: 0.0
  end
end
config =
  ExkPasswd.Config.new!(
    num_words: 2,
    meta: %{transforms: [%BracketTransform{}]}
  )

ExkPasswd.generate(config)

A deterministic transform adds no randomness and may collapse distinct source words to the same output. Built-in Pinyin, Romaji, case, and substitution collisions are included in the seen min-entropy estimate. Random custom-transform callbacks are not trusted as security evidence and receive no rating credit.

Validate a custom output space

password = ExkPasswd.generate(config)
details = ExkPasswd.Entropy.calculate_seen_detailed(config)

%{
  password: password,
  total: Float.round(details.total, 2),
  words: Float.round(details.word_entropy, 2),
  transforms: Float.round(details.transform_entropy, 2),
  digits: Float.round(details.digit_entropy, 2)
}

Runtime presets

Runtime registration requires ExkPasswd.Config.Presets in your supervision tree. Built-in presets do not.

case ExkPasswd.Config.Presets.start_link(nil) do
  {:ok, _pid} -> :ok
  {:error, {:already_started, _pid}} -> :ok
end

custom = ExkPasswd.Config.new!(num_words: 5, separator: "_")
:ok = ExkPasswd.Config.Presets.register(:my_passphrase, custom)
ExkPasswd.generate(:my_passphrase)