Module: Philiprehberger::Password::Zxcvbn

Defined in:
lib/philiprehberger/password/zxcvbn.rb

Constant Summary collapse

LEET_MAP =

L33t substitution mappings

{
  '@' => 'a', '4' => 'a', '^' => 'a',
  '8' => 'b',
  '(' => 'c', '{' => 'c', '<' => 'c',
  '3' => 'e',
  '6' => 'g', '9' => 'g',
  '#' => 'h',
  '1' => 'i', '!' => 'i', '|' => 'i',
  '7' => 'l',
  '0' => 'o',
  '$' => 's', '5' => 's',
  '+' => 't',
  '2' => 'z'
}.freeze
DATE_PATTERNS =

Common date patterns

[
  /\b(19|20)\d{2}\b/, # yyyy
  %r{\b(0?[1-9]|1[0-2])[/-](0?[1-9]|[12]\d|3[01])[/-](\d{2}|\d{4})\b}, # mm/dd/yy or mm/dd/yyyy
  %r{\b(0?[1-9]|[12]\d|3[01])[/-](0?[1-9]|1[0-2])[/-](\d{2}|\d{4})\b}, # dd/mm/yy or dd/mm/yyyy
  /\b(0?[1-9]|1[0-2])(0?[1-9]|[12]\d|3[01])(\d{2}|\d{4})\b/,             # mmddyy or mmddyyyy
  /\b(\d{2}|\d{4})(0?[1-9]|1[0-2])(0?[1-9]|[12]\d|3[01])\b/              # yymmdd or yyyymmdd
].freeze
SPATIAL_PATTERNS_QWERTY =
%w[
  qwert werty ertyu rtyui tyuio yuiop
  asdfg sdfgh dfghj fghjk ghjkl
  zxcvb xcvbn cvbnm
  qwer wert erty rtyu tyui yuio uiop
  asdf sdfg dfgh fghj ghjk hjkl
  zxcv xcvb cvbn vbnm
].freeze
CRACK_TIMES =
{
  0 => 'instant',
  1 => 'minutes',
  2 => 'hours to days',
  3 => 'months to years',
  4 => 'centuries'
}.freeze

Class Method Summary collapse

Class Method Details

.estimate(password) ⇒ Object

Perform zxcvbn-style password strength estimation. Returns a hash with :score (0-4), :patterns (array), and :crack_time_display.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/philiprehberger/password/zxcvbn.rb', line 50

def self.estimate(password)
  pwd = password.to_s
  return { score: 0, patterns: [], crack_time_display: 'instant' } if pwd.empty?

  patterns = []
  patterns.concat(detect_dictionary_words(pwd))
  patterns.concat(detect_leet_substitutions(pwd))
  patterns.concat(detect_spatial_patterns(pwd))
  patterns.concat(detect_date_patterns(pwd))
  patterns.concat(detect_keyboard_patterns(pwd))

  # Calculate base entropy
  base_entropy = Strength.entropy(pwd)

  # Apply penalty for detected patterns
  penalty = patterns.sum { |p| pattern_penalty(p) }
  adjusted_entropy = [base_entropy - penalty, 0.0].max

  score = entropy_to_score(adjusted_entropy, pwd.length, patterns.length)

  {
    score: score,
    patterns: patterns,
    crack_time_display: CRACK_TIMES[score]
  }
end