Skip to content

Password Strength Analyser — Ruby source

Estimate password strength with zxcvbn - realistic dictionary and pattern cracking with crack-time estimates and improvement suggestions. Runs entirely in your browser.

This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# password-strength-analyser — password strength scoring & feedback. Language: Ruby (3.1+, stdlib only). Port of src/lib/password-strength.ts — same self-contained heuristic estimator as this dir's python.py (the live TS/Go twins wrap zxcvbn, whose ranking data is too large to vendor).

module PasswordStrength
  LABELS = ['Very weak', 'Weak', 'Fair', 'Good', 'Strong'].freeze
  THRESHOLDS = [28.0, 36.0, 60.0, 128.0].freeze
  # Curated common-password blocklist — an exact (case-insensitive) match forces score 0.
  COMMON = %w[1234 12345 123123 123456 12345678 123456789 1234567890
              000000 111111 654321 666666 abc123 admin baseball batman dragon
              iloveyou letmein login master monkey passw0rd password password1
              princess qwerty root shadow superman sunshine trustno1 welcome].freeze
  # Keyboard / numeric runs — any length-4 window inside costs 12 bits.
  SEQUENCES = %w[qwertyuiop asdfghjkl zxcvbnm 1234567890 0987654321 abcdefg gfedcba].freeze
  # Strength breakdown — mirrors the TS PasswordAnalysis / Go pwstrength.Result.
  Analysis = Struct.new(:score, :label, :entropy, :crack_time_display, keyword_init: true)

  module_function

  # Guess-pool size: 26 lower + 26 upper + 10 digit + 33 ASCII symbol + 128 other.
  def pool_size(pw)
    lower = pw.match?(/[a-z]/)
    upper = pw.match?(/[A-Z]/)
    digit = pw.match?(/[0-9]/)
    sym = pw.each_char.any? { |c| c.ascii_only? && c !~ /[a-zA-Z0-9]/ }
    other = pw.each_char.any? { |c| !c.ascii_only? }
    (lower ? 26 : 0) + (upper ? 26 : 0) + (digit ? 10 : 0) + (sym ? 33 : 0) + (other ? 128 : 0)
  end

  # Does +lower+ contain any length-4 window of +seq+ (a keyboard/numeric run)?
  def contains_sequence?(lower, seq)
    (0..seq.length - 4).any? { |i| lower.include?(seq[i, 4]) }
  end
  # Human crack time at 1e10 guesses/s — zxcvbn's offline-fast display.
  def crack_time_display(entropy)
    return 'instant' if entropy <= 0 || 2.0**entropy / 1e10 < 1

    seconds = 2.0**entropy / 1e10
    div, name = [[60, 'second'], [3_600, 'minute'], [86_400, 'hour'], [2_592_000, 'month'],
                 [31_536_000, 'year']].reverse.find { |secs, _| seconds >= secs } || [1, 'second']
    v = seconds / div
    return 'centuries' if v >= 1e9
    return format('%.0f million years', v / 1e6) if v >= 1e6
    return format('%.0f thousand years', v / 1e3) if v >= 1e3

    format('%.0f %ss', v, name)
  end

  # Scores 0-4. +user_inputs+ seeds the estimator with related tokens (a substring match weakens the score).
  def analyse(password, user_inputs = [])
    len = password.length
    pool = pool_size(password)
    lp = pool > 1 ? Math.log2(pool) : 0.0
    unique = password.chars.uniq.length
    # Base entropy len*log2(pool), capped by variety — repeats earn quarter credit.
    entropy = pool > 1 ? [len * lp, (unique + (len - unique) * 0.25) * lp].min : 0.0
    lower = password.downcase
    if COMMON.include?(lower)
      entropy = 0.0 # blocklist hit — zxcvbn's dictionary collapse
    else
      SEQUENCES.each { |seq| entropy -= 12 if contains_sequence?(lower, seq) }
      user_inputs.each do |ui|
        l = ui.downcase
        entropy -= 10 if l.length >= 3 && lower.include?(l)
      end
      entropy = 0.0 if entropy.negative?
    end

    score = if len < 4 || entropy < THRESHOLDS[0] then 0
            elsif entropy < THRESHOLDS[1] then 1
            elsif entropy < THRESHOLDS[2] then 2
            elsif entropy < THRESHOLDS[3] then 3
            else 4
            end
    Analysis.new(score:, label: LABELS[score], entropy:, crack_time_display: crack_time_display(entropy))
  end
end

%w[password 12345678 Tr0ub4dour&3 correct\ horse\ battery\ staple u2#9Xq!Lp$7wZ].each do |pw|
  r = PasswordStrength.analyse(pw)
  puts format('%-26s %d/4 %-9s %6.1f bits  %s', pw, r.score, r.label, r.entropy, r.crack_time_display)
end

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →