ExkPasswd security model
Mix.install([
{:exk_passwd, "~> 0.2.0"}
])
This notebook separates generator-aware min-entropy from a naive brute-force search-space estimate. Neither number predicts a specific online service or password hash.
Compare the built-in presets
presets = [:default, :xkcd, :web32, :web16, :wifi, :apple_id, :security]
for preset <- presets do
config = ExkPasswd.Config.Presets.get(preset)
password = ExkPasswd.generate(config)
report = ExkPasswd.Entropy.calculate(password, config)
%{
preset: preset,
length: String.length(password),
seen_min_entropy: Float.round(report.seen, 1),
blind_estimate: Float.round(report.blind, 1),
rating: report.status
}
end
The project rating bands are convenience labels; they are not NIST or OWASP thresholds. Current NIST guidance focuses on verifier behavior such as minimum length, blocklists, rate limiting, and avoiding composition rules for user-chosen passwords.
Inspect an entropy breakdown
config = ExkPasswd.Config.Presets.get(:default)
ExkPasswd.Entropy.calculate_seen_detailed(config)
The calculation assumes the attacker knows the dictionary and configuration. It includes known output collisions and only credits random decisions that can produce distinct values.
Word-count effect
for count <- 2..6 do
config =
ExkPasswd.Config.new!(
num_words: count,
case_transform: :lower,
separator: "-",
digits: {0, 0},
padding: %{char: "", before: 0, after: 0, to_length: 0}
)
{count, Float.round(ExkPasswd.Entropy.calculate_seen(config), 2)}
end
More independent selections from the same pool increase the configured output space linearly in bits. Longer words do not automatically help: restricting the length range can reduce the number of eligible dictionary entries.
A collision caused by a transform
ExkPasswd.Dictionary.load_custom(:pinyin_collision_demo, ["是", "事"])
config =
ExkPasswd.Config.new!(
num_words: 1,
dictionary: :pinyin_collision_demo,
word_length: 1..1,
word_length_bounds: 1..10,
case_transform: :none,
separator: "",
digits: {0, 0},
padding: %{char: "", before: 0, after: 0, to_length: 0},
meta: %{transforms: [%ExkPasswd.Transform.Pinyin{}]}
)
passwords = for _ <- 1..10, do: ExkPasswd.generate(config)
{Enum.uniq(passwords), ExkPasswd.Entropy.calculate_seen_detailed(config)}
Both source characters become shi, so the transformed word contributes zero
bits. Deterministic complexity is not randomness.
Minimum-length padding does not discard entropy
config =
ExkPasswd.Config.new!(
num_words: 5,
word_length: 8..9,
padding: %{char: "=", before: 0, after: 0, to_length: 16}
)
password = ExkPasswd.generate(config)
{String.length(password), password}
The output remains longer than 16; ExkPasswd never silently truncates generated words or digits.
Uniqueness is not a randomness proof
config = ExkPasswd.Config.Presets.get(:xkcd)
passwords = ExkPasswd.Batch.generate_batch(2_000, config)
%{
generated: length(passwords),
unique: length(Enum.uniq(passwords))
}
Few collisions are expected in a large output space, but this experiment cannot
prove cryptographic security or uniformity. Those properties depend on reviewed
use of :crypto.strong_rand_bytes/1 and rejection sampling.
Operational guidance
- Use a password manager and a unique password per account.
- Enable MFA for important accounts.
- Test a destination’s real length and character handling.
- Do not silently truncate passwords during storage or verification.
- Check user-chosen passwords against a breached/common-password blocklist at the verifier; ExkPasswd does not perform that network- or dataset-dependent step.
- Use a slow, memory-hard password hash for stored credentials.
Further reading: