ExkPasswd. Dictionary
(ExkPasswd v0.2.0)
View Source
Dictionary word list management with compile-time optimizations.
This module provides constant-time random word selection through tuple-based storage and pre-transformed case variants.
Optimizations
- Tuple-based storage: Words stored as tuples for constant-time 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
Implementation
- Word selection: Constant-time tuple indexing
- Case transformation: Pre-computed variants eliminate runtime transformation
- Memory cost: ~200KB additional for pre-computed variants
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.
This wordlist provides:
- High entropy: 7,772 words = ~12.92 bits per word
- Memorable words: Common, easy-to-remember English words
- Typability: No complex spellings or obscure words
- Safety: No offensive or problematic words
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.
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 non-empty words (strings)
Examples
iex> words = ["casa", "perro", "gato", "libro"]
...> ExkPasswd.Dictionary.load_custom(:spanish, words)
:okEmpty word lists, empty strings, and non-string entries raise an
ArgumentError.
@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 tuple-based constant-time lookups for efficient word selection.
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(), 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