Module: Zxcvbn::Guesses Private

Includes:
Math
Included in:
Scorer
Defined in:
lib/zxcvbn/guesses.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

Mixin that provides guesses estimation for each match pattern.

Each pattern-specific method returns a raw guess count; #estimate_guesses applies a per-token minimum and memoises the result on the match object. Mirrors the guesses estimation logic from zxcvbn.js v4.

Constant Summary collapse

MIN_GUESSES_BEFORE_GROWING_SEQUENCE =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

10_000
MIN_SUBMATCH_GUESSES_SINGLE_CHAR =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

10
MIN_SUBMATCH_GUESSES_MULTI_CHAR =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

50
BRUTEFORCE_CARDINALITY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

10
MIN_YEAR_SPACE =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

20
START_UPPER =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

/^[A-Z][^A-Z]+$/
END_UPPER =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

/^[^A-Z]+[A-Z]$/
ALL_UPPER =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

/^[^a-z]+$/
ALL_LOWER =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

/^[^A-Z]+$/

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Math

#average_degree_for_graph, #nCk, #starting_positions_for_graph

Instance Attribute Details

#reference_yearObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



206
207
208
# File 'lib/zxcvbn/guesses.rb', line 206

def reference_year
  @reference_year
end

Instance Method Details

#bruteforce_guesses(match) ⇒ Numeric

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns guesses based on token length and assumed cardinality.

Parameters:

Returns:

  • (Numeric)

    guesses based on token length and assumed cardinality



66
67
68
69
70
71
72
# File 'lib/zxcvbn/guesses.rb', line 66

def bruteforce_guesses(match)
  length = match.token ? match.token.length : match.j - match.i + 1
  guesses = BRUTEFORCE_CARDINALITY**length.to_f
  guesses = Float::MAX if guesses.infinite?
  min = length == 1 ? MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1.0 : MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1.0
  [guesses, min].max
end

#date_guesses(match) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns 365 * year_space, multiplied by 4 if a separator is present.

Parameters:

  • match (MatchBuilder)

    a date match with year and separator set

Returns:

  • (Integer)

    365 * year_space, multiplied by 4 if a separator is present



104
105
106
107
108
109
# File 'lib/zxcvbn/guesses.rb', line 104

def date_guesses(match)
  year_space = [(match.year - reference_year).abs, MIN_YEAR_SPACE].max
  guesses = 365 * year_space
  guesses *= 4 if match.separator && !match.separator.empty?
  guesses
end

#dictionary_guesses(match) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns rank multiplied by uppercase and l33t variation counts, plus a factor of 2 if the word was matched in reverse.

Parameters:

Returns:

  • (Integer)

    rank multiplied by uppercase and l33t variation counts, plus a factor of 2 if the word was matched in reverse



150
151
152
153
154
155
156
# File 'lib/zxcvbn/guesses.rb', line 150

def dictionary_guesses(match)
  match.base_guesses = match.rank
  match.uppercase_variations = uppercase_variations(match)
  match.l33t_variations      = l33t_variations(match)
  reversed_multiplier        = match.reversed ? 2 : 1
  match.base_guesses * match.uppercase_variations * match.l33t_variations * reversed_multiplier
end

#digits_guesses(match) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns 10^length (all possible digit strings of that length).

Parameters:

Returns:

  • (Integer)

    10^length (all possible digit strings of that length)



92
93
94
# File 'lib/zxcvbn/guesses.rb', line 92

def digits_guesses(match)
  10**match.token.length
end

#estimate_guesses(match, password) ⇒ Numeric

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Estimate the number of guesses required to crack a match.

Mutates the builder in place: sets guesses, guesses_log10, and any pattern-specific fields (base_guesses, uppercase_variations, l33t_variations). Returns immediately if guesses are already set.

Parameters:

  • match (MatchBuilder)

    the builder to estimate

  • password (String)

    the full password being evaluated

Returns:

  • (Numeric)

    the estimated guess count



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/zxcvbn/guesses.rb', line 35

def estimate_guesses(match, password)
  return match.guesses if match.guesses

  token_length = match.token ? match.token.length : match.j - match.i + 1
  min_guesses =
    if token_length < password.length
      token_length == 1 ? MIN_SUBMATCH_GUESSES_SINGLE_CHAR : MIN_SUBMATCH_GUESSES_MULTI_CHAR
    else
      1
    end

  guesses =
    case match.pattern
    in 'bruteforce' then bruteforce_guesses(match)
    in 'dictionary' then dictionary_guesses(match)
    in 'spatial'    then spatial_guesses(match)
    in 'repeat'     then repeat_guesses(match)
    in 'sequence'   then sequence_guesses(match)
    in 'digits'     then digits_guesses(match)
    in 'year'       then year_guesses(match)
    in 'date'       then date_guesses(match)
    else 1
    end

  match.guesses = [guesses, min_guesses].max
  match.guesses_log10 = ::Math.log10(match.guesses)
  match.guesses
end

#l33t_variations(match) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Count the number of ways the token’s l33t substitutions could have been chosen.

Returns 1 if the match has no l33t substitutions. Otherwise multiplies the variation count for each substituted character pair using combinations.

Parameters:

  • match (MatchBuilder)

    a dictionary match, possibly with l33t substitutions

Returns:

  • (Integer)

    l33t variation multiplier



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/zxcvbn/guesses.rb', line 186

def l33t_variations(match)
  return 1 unless match.l33t && match.sub

  variations = 1
  match.sub.each do |subbed, unsubbed|
    chars        = match.token.downcase.chars
    num_subbed   = chars.count { |c| c == subbed }
    num_unsubbed = chars.count { |c| c == unsubbed }
    if num_subbed.zero? || num_unsubbed.zero?
      variations *= 2
    else
      p = [num_subbed, num_unsubbed].min
      sub_variations = 0
      (1..p).each { |i| sub_variations += nCk(num_subbed + num_unsubbed, i) }
      variations *= sub_variations
    end
  end
  variations
end

#sequence_guesses(match) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns guesses based on sequence type and direction.

Parameters:

  • match (MatchBuilder)

    a sequence match (e.g. “abc”, “6543”)

Returns:

  • (Integer)

    guesses based on sequence type and direction



76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/zxcvbn/guesses.rb', line 76

def sequence_guesses(match)
  first_char = match.token[0]
  base_guesses =
    if %w[a A z Z 0 1 9].include?(first_char)
      4
    elsif first_char.match?(/\d/)
      10
    else
      26
    end
  base_guesses *= 2 unless match.ascending
  base_guesses * match.token.length
end

#spatial_guesses(match) ⇒ Numeric

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns guesses based on graph topology, turns, and shifted keys.

Parameters:

  • match (MatchBuilder)

    a spatial (keyboard pattern) match

Returns:

  • (Numeric)

    guesses based on graph topology, turns, and shifted keys



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/zxcvbn/guesses.rb', line 113

def spatial_guesses(match)
  if %w[qwerty dvorak].include?(match.graph)
    s = starting_positions_for_graph('qwerty')
    d = average_degree_for_graph('qwerty')
  else
    s = starting_positions_for_graph('keypad')
    d = average_degree_for_graph('keypad')
  end

  guesses = 0
  token_length = match.token.length
  turns = match.turns
  (2..token_length).each do |i|
    possible_turns = [turns, i - 1].min
    (1..possible_turns).each do |j|
      guesses += nCk(i - 1, j - 1) * s * (d**j)
    end
  end

  if match.shifted_count&.positive?
    shifted   = match.shifted_count
    unshifted = token_length - match.shifted_count
    if unshifted.zero?
      guesses *= 2
    else
      shift_variations = 0
      (1..[shifted, unshifted].min).each { |i| shift_variations += nCk(shifted + unshifted, i) }
      guesses *= shift_variations
    end
  end

  guesses
end

#uppercase_variations(match) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Count the number of ways the token’s capitalisation could have been chosen.

Returns 1 for all-lowercase or already-lowercase words. Returns 2 for simple patterns (StartUpper, endUPPER, ALLCAPS). Otherwise returns the sum of combinations for mixed-case tokens.

Parameters:

Returns:

  • (Integer)

    uppercase variation multiplier



166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/zxcvbn/guesses.rb', line 166

def uppercase_variations(match)
  word = match.token
  return 1 if word.match?(ALL_LOWER) || word.downcase == word

  [START_UPPER, END_UPPER, ALL_UPPER].each { |r| return 2 if word.match?(r) }

  num_upper = word.chars.count { |c| c.match?(/[A-Z]/) }
  num_lower = word.chars.count { |c| c.match?(/[a-z]/) }
  variations = 0
  (1..[num_upper, num_lower].min).each { |i| variations += nCk(num_upper + num_lower, i) }
  variations
end

#year_guesses(match) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns distance from the current year, floored at MIN_YEAR_SPACE.

Parameters:

Returns:

  • (Integer)

    distance from the current year, floored at MIN_YEAR_SPACE



98
99
100
# File 'lib/zxcvbn/guesses.rb', line 98

def year_guesses(match)
  [(match.token.to_i - reference_year).abs, MIN_YEAR_SPACE].max
end