Powered by AppSignal & Oban Pro

ExkPasswd Security Analysis

notebooks/security.livemd

ExkPasswd Security Analysis

Mix.install([
  {:exk_passwd, "~> 0.1.0"},
  {:kino, "~> 0.12"}
])

Introduction

This notebook explores the security aspects of ExkPasswd-generated passwords:

  • Entropy - How much randomness/unpredictability
  • Strength ratings - Weak, moderate, strong, very strong
  • Crack time estimates - How long to brute force
  • Comparison - Different configurations and their security

What is Entropy?

Entropy measures the unpredictability of a password in bits. Higher is better.

  • Each bit doubles the number of possibilities
  • 40 bits = 1 trillion possibilities
  • 60 bits = 1 quintillion possibilities
  • 128 bits = effectively uncrackable

Calculating Entropy

ExkPasswd provides built-in entropy calculation:

config = ExkPasswd.Config.Presets.get(:default)
password = ExkPasswd.generate(config)
entropy_result = ExkPasswd.Entropy.calculate(password, config)

IO.puts("Password: #{password}")
IO.puts("Blind Entropy: #{Float.round(entropy_result.blind, 2)} bits")
IO.puts("Seen Entropy: #{Float.round(entropy_result.seen, 2)} bits")
IO.puts("Status: #{entropy_result.status}")
IO.puts("Crack time (blind): #{entropy_result.blind_crack_time}")
IO.puts("Crack time (seen): #{entropy_result.seen_crack_time}")

Strength Analysis

Get a comprehensive strength analysis:

config = ExkPasswd.Config.Presets.get(:security)
password = ExkPasswd.generate(config)
strength = ExkPasswd.Strength.analyze(password, config)

IO.puts("Password: #{password}")
IO.puts("Rating: #{strength.rating}")
IO.puts("Score: #{strength.score}/100")
IO.puts("Entropy: #{Float.round(strength.entropy_bits, 2)} bits")
IO.puts("Length: #{String.length(password)} characters")

Comparing Presets

Let’s compare the security of different presets:

presets = [:xkcd, :web32, :wifi, :security, :appleid, :default]

results =
  for preset <- presets do
    config = ExkPasswd.Config.Presets.get(preset)
    password = ExkPasswd.generate(config)
    strength = ExkPasswd.Strength.analyze(password, config)

    %{
      preset: preset,
      password: password,
      length: String.length(password),
      entropy: Float.round(strength.entropy_bits, 1),
      rating: strength.rating,
      score: strength.score
    }
  end

# Display as table
IO.puts("\n| Preset | Length | Entropy | Rating | Score |")
IO.puts("|--------|--------|---------|--------|-------|")

for r <- results do
  IO.puts(
    "| #{r.preset} | #{r.length} | #{r.entropy} bits | #{r.rating} | #{r.score}/100 |"
  )
end

:ok

Word Count Impact

How does the number of words affect security?

alias ExkPasswd.Config

word_counts = [2, 3, 4, 5, 6]

IO.puts("\n=== Impact of Word Count on Security ===\n")

for count <- word_counts do
  config = Config.new!(num_words: count, case_transform: :lower, separator: "-")
  password = ExkPasswd.generate(config)
  strength = ExkPasswd.Strength.analyze(password, config)

  IO.puts("#{count} words: #{password}")
  IO.puts("  Entropy: #{Float.round(strength.entropy_bits, 2)} bits")
  IO.puts("  Rating: #{strength.rating}")
  IO.puts("  Score: #{strength.score}/100\n")
end

:ok

Word Length Impact

Does using longer words make passwords more secure?

length_configs = [
  {3, 4, "very short"},
  {4, 6, "short"},
  {6, 8, "medium"},
  {8, 10, "long"},
  {10, 12, "very long"}
]

IO.puts("\n=== Impact of Word Length on Security ===\n")

for {min, max, label} <- length_configs do
  config =
    Config.new!(
      num_words: 3,
      word_length: min..max,
      separator: "-"
    )

  password = ExkPasswd.generate(config)
  strength = ExkPasswd.Strength.analyze(password, config)

  IO.puts("#{label} words (#{min}-#{max} chars): #{password}")
  IO.puts("  Entropy: #{Float.round(strength.entropy_bits, 2)} bits")
  IO.puts("  Rating: #{strength.rating}\n")
end

:ok

Case Transform Impact

How do case transformations affect security?

transforms = [
  :lower,
  :upper,
  :capitalize,
  :alternate,
  :random,
  :invert
]

IO.puts("\n=== Impact of Case Transform on Security ===\n")

for transform <- transforms do
  config = Config.new!(num_words: 3, case_transform: transform, separator: "-")
  password = ExkPasswd.generate(config)
  strength = ExkPasswd.Strength.analyze(password, config)

  IO.puts("#{transform}: #{password}")
  IO.puts("  Entropy: #{Float.round(strength.entropy_bits, 2)} bits")
  IO.puts("  Rating: #{strength.rating}\n")
end

:ok

Padding Impact

How much does adding numbers and symbols help?

padding_configs = [
  {0, 0, 0, 0, "No padding"},
  {2, 0, 0, 0, "2 digits before"},
  {2, 2, 0, 0, "2 digits before & after"},
  {3, 3, 0, 0, "3 digits before & after"},
  {2, 2, 1, 1, "Digits + symbols"}
]

IO.puts("\n=== Impact of Padding on Security ===\n")

for {db, da, sb, sa, label} <- padding_configs do
  config =
    Config.new!(
      num_words: 3,
      digits: {db, da},
      padding: %{char: "!@#$%^&*", before: sb, after: sa, to_length: 0}
    )

  password = ExkPasswd.generate(config)
  strength = ExkPasswd.Strength.analyze(password, config)

  IO.puts("#{label}: #{password}")
  IO.puts("  Entropy: #{Float.round(strength.entropy_bits, 2)} bits")
  IO.puts("  Rating: #{strength.rating}\n")
end

:ok

Cryptographic Randomness

ExkPasswd uses :crypto.strong_rand_bytes/1 for all random operations. This is:

  • Cryptographically secure - Unpredictable even with knowledge of previous outputs
  • High-quality - Passes statistical randomness tests
  • OS-provided - Uses the operating system’s entropy pool

Let’s verify randomness by generating many passwords and checking for duplicates:

# Generate 10,000 passwords and check uniqueness
count = 10_000
passwords = ExkPasswd.Batch.generate_batch(count, :default)
unique_count = Enum.uniq(passwords) |> length()
duplicate_count = count - unique_count

IO.puts("Generated: #{count} passwords")
IO.puts("Unique: #{unique_count}")
IO.puts("Duplicates: #{duplicate_count}")
IO.puts("Uniqueness: #{Float.round(unique_count / count * 100, 2)}%")

if duplicate_count == 0 do
  IO.puts("\n✓ Perfect! No collisions detected.")
else
  IO.puts("\n⚠ Some duplicates found (expected with high volumes)")
end

Security Recommendations

Based on this analysis:

Minimum Security

For basic security (personal accounts, low-risk):

basic_config = Config.new!(num_words: 3, digits: {0, 2})
basic_pwd = ExkPasswd.generate(basic_config)
basic_strength = ExkPasswd.Strength.analyze(basic_pwd, basic_config)

IO.puts("Basic security config:")
IO.puts("Password: #{basic_pwd}")
IO.puts("Entropy: #{Float.round(basic_strength.entropy_bits, 2)} bits (aim for 50+)")

Strong Security

For important accounts (email, banking, work):

strong_config =
  Config.new!(
    num_words: 4,
    case_transform: :capitalize,
    digits: {2, 2}
  )

strong_pwd = ExkPasswd.generate(strong_config)
strong_strength = ExkPasswd.Strength.analyze(strong_pwd, strong_config)

IO.puts("Strong security config:")
IO.puts("Password: #{strong_pwd}")
IO.puts("Entropy: #{Float.round(strong_strength.entropy_bits, 2)} bits (aim for 70+)")

Maximum Security

For critical systems (servers, encryption keys, admin accounts):

max_config = ExkPasswd.Config.Presets.get(:security)
max_pwd = ExkPasswd.generate(max_config)
max_strength = ExkPasswd.Strength.analyze(max_pwd, max_config)

IO.puts("Maximum security config:")
IO.puts("Password: #{max_pwd}")
IO.puts("Entropy: #{Float.round(max_strength.entropy_bits, 2)} bits (aim for 100+)")

Interactive Security Explorer

Try different configurations and see their security impact:

# Modify these values and re-run to experiment
my_config =
  Config.new(
    num_words: 4,
    word_length_min: 5,
    word_length_max: 8,
    case_transform: :capitalize,
    separator: "-",
    digits: {2, 2},
    padding: %{char: "!@#$", before: 0, after: 1, to_length: 0}
  )

password = ExkPasswd.generate(my_config)
strength = ExkPasswd.Strength.analyze(password, my_config)

IO.puts("Your password: #{password}")
IO.puts("\n=== Security Analysis ===")
IO.puts("Length: #{String.length(password)} characters")
IO.puts("Entropy: #{Float.round(strength.entropy_bits, 2)} bits")
IO.puts("Rating: #{strength.rating}")
IO.puts("Score: #{strength.score}/100")

Next Steps

Further Reading