ExkPasswd. Dictionary
(ExkPasswd v0.3.0)
View Source
Manages the bundled EFF word list and application-supplied dictionaries.
Common EFF length ranges and case variants are compiled into tuples for direct indexed selection. Other ranges are assembled from a small by-length index.
Optimizations
- Tuple-based storage: Precomputed ranges support direct indexed access
- Pre-transformed cases: Separate uppercase/lowercase/capitalized variants
- Pre-computed ranges: Common word length ranges pre-computed at compile time
- Custom dictionary support: Runtime
:persistent_termstorage for user dictionaries
Word List Source
The word list is the EFF Large Wordlist (7,772 of its 7,776 words), developed by the Electronic Frontier Foundation specifically for passphrase generation: https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases
The four hyphenated entries (drop-down, felt-tip, t-shirt, yo-yo)
are excluded so every word is strictly lowercase a-z and cannot collide
with separator characters. See docs/SECURITY.md for checksums and
provenance.
Selecting uniformly from all 7,772 entries would provide about 12.92 bits per word. A configured length range usually selects from a smaller subset.
Custom Dictionaries
You can load custom dictionaries at runtime for specific use cases:
ExkPasswd.Dictionary.load_custom(:spanish, ["casa", "perro", "gato", ...])
ExkPasswd.Dictionary.random_word_between(4, 8, :none, :spanish)Custom dictionaries are stored in :persistent_term, so they survive the
process that loaded them and reads are zero-copy. Loading (or deleting) a
dictionary triggers a global GC scan — load dictionaries once at application
start rather than in hot paths. Use delete_custom/1 to remove one.
Examples
iex> ExkPasswd.Dictionary.size()
7772
iex> word = ExkPasswd.Dictionary.random_word_between(4, 8)
...> len = String.length(word)
...> len >= 4 and len <= 8
true
iex> word = ExkPasswd.Dictionary.random_word_between(5, 7, :capitalize)
...> len = String.length(word)
...> len >= 5 and len <= 7
true
iex> String.first(word) == String.upcase(String.first(word))
true
Summary
Functions
Returns all words in the default dictionary.
Returns the count of words between min and max length (inclusive).
Delete a previously loaded custom dictionary.
No-op kept for backwards compatibility.
Load a custom dictionary for runtime use.
Returns the maximum word length in the default dictionary.
Returns the minimum word length in the default dictionary.
Returns a random word between min and max length with optional case transformation.
Select a random word using a stateful Buffer generator.
Returns the total number of words in the default dictionary.
Returns the unique dictionary outputs available for a length range and case variant.
Functions
@spec all() :: [String.t()]
Returns all words in the default dictionary.
Examples
iex> words = ExkPasswd.Dictionary.all()
...> is_list(words)
true
iex> length(words) > 0
true
@spec count_between(pos_integer(), pos_integer(), atom()) :: non_neg_integer()
Returns the count of words between min and max length (inclusive).
Supports both default :eff dictionary and custom dictionaries.
Unknown custom dictionaries return 0 rather than raising. Callers such as
ExkPasswd.Entropy treat that as zero word entropy, which degrades
conservatively (entropy is understated, never overstated).
Parameters
min- Minimum word length (inclusive)max- Maximum word length (inclusive)dict- Dictionary to use (:effor custom name, default:eff)
Examples
iex> count = ExkPasswd.Dictionary.count_between(4, 8)
...> is_integer(count) and count > 0
true
@spec delete_custom(atom()) :: :ok
Delete a previously loaded custom dictionary.
Returns :ok whether or not the dictionary existed. Like load_custom/2,
this updates :persistent_term and triggers a global GC scan, so prefer
loading dictionaries once over repeated load/delete cycles.
Examples
iex> ExkPasswd.Dictionary.load_custom(:temporary, ["uno", "dos", "tres"])
...> ExkPasswd.Dictionary.delete_custom(:temporary)
:ok
@spec init() :: :ok
No-op kept for backwards compatibility.
Earlier versions stored custom dictionaries in an ETS table that required
initialization. Custom dictionaries now live in :persistent_term, which
needs no setup, so calling this function is no longer necessary.
Load a custom dictionary for runtime use.
The dictionary is stored in :persistent_term and can be referenced by name
when generating passwords. Storage is process-independent: the dictionary
remains available even after the process that loaded it exits.
Loading a dictionary triggers a global GC scan (a property of
:persistent_term updates), so load dictionaries once at application start
rather than in hot paths.
Parameters
name- Atom identifier for the dictionarywordlist- Non-empty list of unique, valid UTF-8 words
Examples
iex> words = ["casa", "perro", "gato", "libro"]
...> ExkPasswd.Dictionary.load_custom(:spanish, words)
:okWords are normalized to Unicode NFC. Empty lists, invalid strings, and words
that become duplicates after normalization raise ArgumentError. Case
variants that produce the same output are stored once so every reachable
output remains uniformly selectable.
@spec max_length() :: pos_integer()
Returns the maximum word length in the default dictionary.
Examples
iex> ExkPasswd.Dictionary.max_length()
9
@spec min_length() :: pos_integer()
Returns the minimum word length in the default dictionary.
Examples
iex> ExkPasswd.Dictionary.min_length()
3
@spec random_word_between(pos_integer(), pos_integer(), atom(), atom()) :: String.t() | nil
Returns a random word between min and max length with optional case transformation.
Uses a precomputed tuple for common EFF ranges and a by-length fallback for other ranges.
Parameters
min- Minimum word length (inclusive)max- Maximum word length (inclusive)case_transform- Case transform to apply (:none,:lower,:upper,:capitalize)dict- Dictionary to use (:effor custom name)
Returns
A random word with the specified length and case, or nil if none exist.
Examples
iex> word = ExkPasswd.Dictionary.random_word_between(4, 8)
...> len = String.length(word)
...> len >= 4 and len <= 8
true
iex> word = ExkPasswd.Dictionary.random_word_between(5, 7, :upper)
...> word == String.upcase(word)
true
@spec random_word_between_with_state( non_neg_integer(), non_neg_integer(), atom(), atom(), ExkPasswd.Buffer.t() ) :: {String.t() | nil, ExkPasswd.Buffer.t()}
Select a random word using a stateful Buffer generator.
This is an optimized version for batch generation that accepts and returns
a Buffer state, reducing the number of :crypto.strong_rand_bytes/1
syscalls.
Parameters
min- Minimum word lengthmax- Maximum word lengthcase_transform- Case transformation to applydict- Dictionary name (default: :eff)random_state- ABuffer.t()state
Returns
A tuple of {word, new_random_state}
Examples
iex> alias ExkPasswd.Buffer
...> state = Buffer.new(1_000)
...>
...> {word, _new_state} =
...> ExkPasswd.Dictionary.random_word_between_with_state(4, 8, :none, :eff, state)
...>
...> len = String.length(word)
...> len >= 4 and len <= 8
true
@spec size() :: pos_integer()
Returns the total number of words in the default dictionary.
Examples
iex> ExkPasswd.Dictionary.size()
7772
@spec words_between(pos_integer(), pos_integer(), atom(), atom()) :: [String.t()]
Returns the unique dictionary outputs available for a length range and case variant.
This is primarily useful for auditing a configured output space. Unknown custom dictionaries return an empty list.
Parameters
min- Minimum word length, inclusivemax- Maximum word length, inclusivecase_transform- One of:none,:lower,:upper, or:capitalizedict-:effor the name of a loaded custom dictionary
Examples
iex> words = ExkPasswd.Dictionary.words_between(4, 4)
...> length(words) > 0 and Enum.all?(words, &(String.length(&1) == 4))
true