Password Strength Analyzer

Analyze password strength and check against Have I Been Pwned breach database

[ AUTHENTICATION SECURITY ]

INPUT PARAMETERS

🔒 Transparency Notice

⚠️ Important: This tool is for testing password strength patterns only. Never test passwords that are currently in use.

This tool analyzes passwords in memory for:

  • Length and complexity
  • Character types (uppercase, lowercase, numbers, special)
  • Common patterns and sequences
  • Bits of entropy
  • Estimated crack time
  • Whether it appears in breach databases (via Have I Been Pwned)

✓ Your password is NEVER stored or logged - it's only processed in memory for analysis. The complete source code is available below for review.

▶ VIEW SOURCE CODE (Click to expand)
📄 password_strength_tool.rb
# frozen_string_literal: true

require 'digest'
require 'net/http'

# Password Strength Analyzer with Have I Been Pwned Integration
# Analyzes password strength and checks against breach database
class PasswordStrengthTool
  include SecurityTool

  configure_tool(
    name: "Password Strength Analyzer",
    description: "Analyze password strength and check against Have I Been Pwned breach database",
    category: "Authentication Security"
  )

  input_field :password, type: :password, label: "Password to Test", placeholder: "Enter password", required: true
  output_format :html, :json

  def execute(params)
    password = params[:password]

    # Calculate basic metrics
    score = calculate_score(password)
    feedback = generate_feedback(password, score)
    entropy = calculate_entropy(password)
    crack_time = estimate_crack_time(password)

    # Check against Have I Been Pwned
    pwned_data = check_pwned(password)

    {
      password_length: password.length,
      score: score,
      strength: strength_label(score),
      entropy_bits: entropy,
      estimated_crack_time: crack_time,
      character_types: analyze_character_types(password),
      pwned_status: pwned_data[:status],
      pwned_count: pwned_data[:count],
      feedback: feedback
    }
  end

  private

  def calculate_score(password)
    score = 0

    # Length scoring
    score += 1 if password.length >= 8
    score += 1 if password.length >= 12
    score += 1 if password.length >= 16
    score += 1 if password.length >= 20

    # Character diversity
    score += 1 if password.match?(/[a-z]/)
    score += 1 if password.match?(/[A-Z]/)
    score += 1 if password.match?(/[0-9]/)
    score += 1 if password.match?(/[^a-zA-Z0-9]/)

    # Pattern detection (penalties)
    score -= 1 if password.match?(/(.)\1{2,}/) # Repeated characters
    score -= 1 if password.match?(/123|234|345|456|567|678|789|890/) # Sequential numbers
    score -= 1 if password.match?(/abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz/i) # Sequential letters
    score -= 2 if common_password?(password)

    [ [ score, 0 ].max, 10 ].min
  end

  def strength_label(score)
    case score
    when 0..2 then "Very Weak"
    when 3..4 then "Weak"
    when 5..6 then "Moderate"
    when 7..8 then "Strong"
    else "Very Strong"
    end
  end

  def analyze_character_types(password)
    types = []
    types << "lowercase" if password.match?(/[a-z]/)
    types << "uppercase" if password.match?(/[A-Z]/)
    types << "numbers" if password.match?(/[0-9]/)
    types << "special" if password.match?(/[^a-zA-Z0-9]/)
    types.join(", ")
  end

  def calculate_entropy(password)
    charset_size = 0
    charset_size += 26 if password.match?(/[a-z]/)
    charset_size += 26 if password.match?(/[A-Z]/)
    charset_size += 10 if password.match?(/[0-9]/)
    charset_size += 32 if password.match?(/[^a-zA-Z0-9]/)

    return 0 if charset_size == 0

    (password.length * Math.log2(charset_size)).round(2)
  end

  def estimate_crack_time(password)
    entropy = calculate_entropy(password)

    # Assuming 10 billion guesses per second (modern GPU)
    seconds = (2 ** entropy) / 10_000_000_000

    format_time(seconds)
  end

  def format_time(seconds)
    return "Instant" if seconds < 1
    return "#{seconds.round} seconds" if seconds < 60
    return "#{(seconds / 60).round} minutes" if seconds < 3600
    return "#{(seconds / 3600).round} hours" if seconds < 86400
    return "#{(seconds / 86400).round} days" if seconds < 2592000
    return "#{(seconds / 2592000).round} months" if seconds < 31536000

    years = (seconds / 31536000).round
    return "#{years} years" if years < 1000
    return "#{years} years (millennia)" if years < 1_000_000
    "#{years} years (geological timescale)"
  end

  def common_password?(password)
    # Top 100 most common passwords
    common = %w[
      password 123456 12345678 qwerty abc123 monkey letmein trustno1 dragon
      baseball iloveyou master sunshine princess football shadow 123123
      654321 superman michael ninja mustang password1 123456789 password123
      welcome login admin 1234567890 solo passw0rd starwars jesus 1234567
      1234 666666 mypass fuck password321 696969 1q2w3e4r qwertyuiop
      computer donald michael1 daniel sunshine1 michelle computer1 freedom
      whatever lovely buster jennifer babygirl family2012 liverpool
      iloveyou1 football1 charlie pokemon secret superman1 love123 dallas
      london ashley 12345 pepper george charlie1 123321 summer hunter
      target love samsung hello1 ashley1 anthony charlie2 oliver cookie
      orange amanda jessica zxcvbnm michelle1 party ranger access whatever1
      michelle2 love jordan google jackson batman lovely1 money
    ]

    common.include?(password.downcase)
  end

  def check_pwned(password)
    begin
      # Create SHA-1 hash of password
      hash = Digest::SHA1.hexdigest(password).upcase
      prefix = hash[0..4]
      suffix = hash[5..-1]

      # Query Have I Been Pwned API (k-Anonymity model)
      uri = URI("https://api.pwnedpasswords.com/range/#{prefix}")
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = true
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE
      http.open_timeout = 5
      http.read_timeout = 5

      request = Net::HTTP::Get.new(uri)
      request['User-Agent'] = 'SecTools-PasswordChecker'

      response = http.request(request)

      if response.code == '200'
        # Parse response to find suffix match
        response.body.each_line do |line|
          hash_suffix, count = line.strip.split(':')
          if hash_suffix == suffix
            count_num = count.to_i
            return {
              status: "COMPROMISED",
              count: count_num,
              message: "This password has been seen #{count_num} times in data breaches"
            }
          end
        end

        return {
          status: "SAFE",
          count: 0,
          message: "Not found in breach database"
        }
      else
        puts "not 200: #{response.code}"
        return {
          status: "ERROR",
          count: nil,
          message: "Could not check breach database (API error)"
        }
      end
    rescue StandardError => e
      puts e.message
      return {
        status: "ERROR",
        count: nil,
        message: "Could not check breach database: #{e.message}"
      }
    end
  end
end

PasswordStrengthTool.register!

🔒 Security Notes:

  • • Your password is only processed in memory and never stored
  • • Have I Been Pwned uses k-Anonymity: only the first 5 characters of the SHA-1 hash are sent
  • • The full password hash never leaves your device
  • • All processing happens server-side in real-time
  • • No logs are kept of passwords tested