zxcvbn-ruby 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -1
  3. data/README.md +322 -75
  4. data/data/frequency_lists/english_wikipedia.txt +30000 -0
  5. data/data/frequency_lists/female_names.txt +11 -114
  6. data/data/frequency_lists/male_names.txt +3 -24
  7. data/data/frequency_lists/passwords.txt +29623 -6764
  8. data/data/frequency_lists/surnames.txt +28 -30611
  9. data/data/frequency_lists/{english.txt → us_tv_and_film.txt} +147 -13532
  10. data/lib/zxcvbn/clock.rb +6 -0
  11. data/lib/zxcvbn/crack_time.rb +52 -18
  12. data/lib/zxcvbn/data.rb +61 -21
  13. data/lib/zxcvbn/dictionary_ranker.rb +10 -0
  14. data/lib/zxcvbn/feedback.rb +11 -6
  15. data/lib/zxcvbn/feedback_giver.rb +75 -50
  16. data/lib/zxcvbn/guesses.rb +208 -0
  17. data/lib/zxcvbn/match.rb +95 -15
  18. data/lib/zxcvbn/match_builder.rb +15 -0
  19. data/lib/zxcvbn/matchers/date.rb +171 -106
  20. data/lib/zxcvbn/matchers/dictionary.rb +15 -8
  21. data/lib/zxcvbn/matchers/digits.rb +6 -1
  22. data/lib/zxcvbn/matchers/l33t.rb +30 -34
  23. data/lib/zxcvbn/matchers/regex_helpers.rb +14 -6
  24. data/lib/zxcvbn/matchers/repeat.rb +47 -16
  25. data/lib/zxcvbn/matchers/sequences.rb +58 -48
  26. data/lib/zxcvbn/matchers/spatial.rb +22 -6
  27. data/lib/zxcvbn/matchers/year.rb +6 -1
  28. data/lib/zxcvbn/math.rb +15 -28
  29. data/lib/zxcvbn/omnimatch.rb +70 -22
  30. data/lib/zxcvbn/ruby.rb +3 -0
  31. data/lib/zxcvbn/score.rb +34 -10
  32. data/lib/zxcvbn/scorer.rb +142 -75
  33. data/lib/zxcvbn/tester.rb +58 -23
  34. data/lib/zxcvbn/tester_builder.rb +83 -0
  35. data/lib/zxcvbn/trie.rb +21 -0
  36. data/lib/zxcvbn/version.rb +1 -1
  37. data/lib/zxcvbn.rb +47 -7
  38. data/sig/README.md +65 -0
  39. data/sig/zxcvbn/clock.rbs +5 -0
  40. data/sig/zxcvbn/crack_time.rbs +11 -0
  41. data/sig/zxcvbn/data.rbs +40 -0
  42. data/sig/zxcvbn/dictionary_ranker.rbs +7 -0
  43. data/sig/zxcvbn/feedback.rbs +10 -0
  44. data/sig/zxcvbn/feedback_giver.rbs +13 -0
  45. data/sig/zxcvbn/guesses.rbs +36 -0
  46. data/sig/zxcvbn/match.rbs +40 -0
  47. data/sig/zxcvbn/match_builder.rbs +36 -0
  48. data/sig/zxcvbn/matchers/date.rbs +23 -0
  49. data/sig/zxcvbn/matchers/dictionary.rbs +21 -0
  50. data/sig/zxcvbn/matchers/digits.rbs +11 -0
  51. data/sig/zxcvbn/matchers/l33t.rbs +27 -0
  52. data/sig/zxcvbn/matchers/regex_helpers.rbs +7 -0
  53. data/sig/zxcvbn/matchers/repeat.rbs +11 -0
  54. data/sig/zxcvbn/matchers/sequences.rbs +16 -0
  55. data/sig/zxcvbn/matchers/spatial.rbs +15 -0
  56. data/sig/zxcvbn/matchers/year.rbs +11 -0
  57. data/sig/zxcvbn/math.rbs +9 -0
  58. data/sig/zxcvbn/omnimatch.rbs +19 -0
  59. data/sig/zxcvbn/score.rbs +26 -0
  60. data/sig/zxcvbn/scorer.rbs +19 -0
  61. data/sig/zxcvbn/tester.rbs +15 -0
  62. data/sig/zxcvbn/tester_builder.rbs +16 -0
  63. data/sig/zxcvbn/trie.rbs +17 -0
  64. data/sig/zxcvbn.rbs +12 -0
  65. metadata +46 -12
  66. data/lib/zxcvbn/entropy.rb +0 -158
  67. data/lib/zxcvbn/matchers/new_l33t.rb +0 -118
  68. data/lib/zxcvbn/password_strength.rb +0 -27
@@ -1,19 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'zxcvbn/match'
3
+ require 'zxcvbn/match_builder'
4
4
 
5
5
  module Zxcvbn
6
6
  module Matchers
7
- # Given a password and a dictionary, match on any sequential segment of
8
- # the lowercased password in the dictionary
9
-
7
+ # Matches any sequential segment of the lowercased password that appears in
8
+ # a ranked dictionary.
9
+ # @api private
10
10
  class Dictionary
11
+ # @param name [String] dictionary identifier used in match results
12
+ # @param ranked_dictionary [Hash{String => Integer}] lowercased word → rank
13
+ # @param trie [Trie, nil] optional prefix trie for faster lookups
11
14
  def initialize(name, ranked_dictionary, trie = nil)
12
15
  @name = name
13
16
  @ranked_dictionary = ranked_dictionary
14
17
  @trie = trie
15
18
  end
16
19
 
20
+ # Returns all dictionary matches found in password.
21
+ #
22
+ # @param password [String]
23
+ # @return [Array<MatchBuilder>] matches with pattern "dictionary"
17
24
  def matches(password)
18
25
  lowercased_password = password.downcase
19
26
 
@@ -56,12 +63,12 @@ module Zxcvbn
56
63
  end
57
64
 
58
65
  def build_match(matched_word, token, start_pos, end_pos, rank)
59
- Match.new(
60
- matched_word: matched_word,
61
- token: token,
66
+ MatchBuilder.new(
67
+ matched_word:,
68
+ token:,
62
69
  i: start_pos,
63
70
  j: end_pos,
64
- rank: rank,
71
+ rank:,
65
72
  pattern: 'dictionary',
66
73
  dictionary_name: @name
67
74
  )
@@ -4,11 +4,16 @@ require 'zxcvbn/matchers/regex_helpers'
4
4
 
5
5
  module Zxcvbn
6
6
  module Matchers
7
+ # Matches runs of 3 or more consecutive digits in the password.
8
+ # @api private
7
9
  class Digits
8
10
  include RegexHelpers
9
11
 
10
- DIGITS_REGEX = /\d{3,}/.freeze
12
+ # Matches runs of 3 or more consecutive digits.
13
+ DIGITS_REGEX = /\d{3,}/
11
14
 
15
+ # @param password [String]
16
+ # @return [Array<MatchBuilder>] matches with pattern "digits"
12
17
  def matches(password)
13
18
  result = []
14
19
  re_match_all(DIGITS_REGEX, password) do |match|
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  module Zxcvbn
6
4
  module Matchers
5
+ # Matches dictionary words after substituting common l33t-speak character
6
+ # replacements (e.g. "@" for "a", "3" for "e").
7
+ # @api private
7
8
  class L33t
9
+ # Mapping from plain letter to the l33t characters that can represent it.
8
10
  L33T_TABLE = {
9
11
  'a' => ['4', '@'].freeze,
10
12
  'b' => ['8'].freeze,
@@ -20,10 +22,15 @@ module Zxcvbn
20
22
  'z' => ['2'].freeze
21
23
  }.freeze
22
24
 
25
+ # @param dictionary_matchers [Array<Dictionary>] matchers to run against substituted passwords
23
26
  def initialize(dictionary_matchers)
24
27
  @dictionary_matchers = dictionary_matchers
25
28
  end
26
29
 
30
+ # Returns l33t-substituted dictionary matches found in password.
31
+ #
32
+ # @param password [String]
33
+ # @return [Array<MatchBuilder>] matches with pattern "dictionary" and l33t: true
27
34
  def matches(password)
28
35
  matches = []
29
36
  lowercased_password = password.downcase
@@ -44,14 +51,19 @@ module Zxcvbn
44
51
  matches
45
52
  end
46
53
 
54
+ # Returns a copy of password with each character replaced according to sub.
55
+ #
56
+ # @param password [String]
57
+ # @param sub [Hash{String => String}] character substitution map
58
+ # @return [String]
47
59
  def translate(password, sub)
48
- result = String.new
49
- password.each_char do |chr|
50
- result << (sub[chr] || chr)
51
- end
52
- result
60
+ password.gsub(Regexp.union(sub.keys), sub)
53
61
  end
54
62
 
63
+ # Returns the subset of {L33T_TABLE} whose l33t characters appear in password.
64
+ #
65
+ # @param password [String] lowercased password
66
+ # @return [Hash{String => Array<String>}]
55
67
  def relevent_l33t_subtable(password)
56
68
  filtered = {}
57
69
  L33T_TABLE.each do |letter, subs|
@@ -61,19 +73,15 @@ module Zxcvbn
61
73
  filtered
62
74
  end
63
75
 
76
+ # Enumerates all possible substitution combinations for the given l33t subtable.
77
+ #
78
+ # @param table [Hash{String => Array<String>}] relevant l33t subtable
79
+ # @return [Array<Hash{String => String}>] list of substitution maps to try
64
80
  def l33t_subs(table)
65
81
  keys = table.keys
66
82
  subs = [[]]
67
83
  subs = find_substitutions(subs, table, keys)
68
- new_subs = []
69
- subs.each do |sub|
70
- hash = {}
71
- sub.each do |l33t_char, chr|
72
- hash[l33t_char] = chr
73
- end
74
- new_subs << hash
75
- end
76
- new_subs
84
+ subs.map(&:to_h)
77
85
  end
78
86
 
79
87
  private
@@ -83,16 +91,11 @@ module Zxcvbn
83
91
  token = password.slice(match.i, length)
84
92
  return if token.downcase == match.matched_word.downcase
85
93
 
86
- match_substitutions = {}
87
- substitution.each do |s, letter|
88
- match_substitutions[s] = letter if token.include?(s)
89
- end
94
+ match_substitutions = substitution.select { |s, _| token.include?(s) }
90
95
  match.l33t = true
91
96
  match.token = token
92
97
  match.sub = match_substitutions
93
- match.sub_display = match_substitutions.map do |k, v|
94
- "#{k} -> #{v}"
95
- end.join(', ')
98
+ match.sub_display = match_substitutions.map { |k, v| "#{k} -> #{v}" }.join(', ')
96
99
  matches << match
97
100
  end
98
101
 
@@ -100,21 +103,14 @@ module Zxcvbn
100
103
  return subs if keys.empty?
101
104
 
102
105
  first_key = keys[0]
103
- rest_keys = keys[1..-1]
106
+ rest_keys = keys[1..]
104
107
  next_subs = []
105
108
  table[first_key].each do |l33t_char|
106
109
  subs.each do |sub|
107
- dup_l33t_index = -1
108
- (0...sub.length).each do |i|
109
- if sub[i][0] == l33t_char
110
- dup_l33t_index = i
111
- break
112
- end
113
- end
110
+ dup_l33t_index = sub.find_index { |pair| pair[0] == l33t_char }
114
111
 
115
- if dup_l33t_index == -1
116
- sub_extension = sub + [[l33t_char, first_key]]
117
- next_subs << sub_extension
112
+ if dup_l33t_index.nil?
113
+ next_subs << (sub + [[l33t_char, first_key]])
118
114
  else
119
115
  sub_alternative = sub.dup
120
116
  sub_alternative[dup_l33t_index, 1] = [[l33t_char, first_key]]
@@ -1,10 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'zxcvbn/match'
3
+ require 'zxcvbn/match_builder'
4
4
 
5
5
  module Zxcvbn
6
+ # Namespace for all password pattern matchers.
7
+ # @api private
6
8
  module Matchers
9
+ # Shared helper for iterating non-overlapping regex matches over a password.
10
+ # @api private
7
11
  module RegexHelpers
12
+ # Yields a {Match} and the underlying MatchData for every non-overlapping
13
+ # occurrence of regex in password.
14
+ #
15
+ # @param regex [Regexp] pattern to search for
16
+ # @param password [String] the password to search
17
+ # @yieldparam match [MatchBuilder] match with i, j, and token set
18
+ # @yieldparam re_match [MatchData] the underlying MatchData object
19
+ # @return [void]
8
20
  def re_match_all(regex, password)
9
21
  pos = 0
10
22
  while (re_match = regex.match(password, pos))
@@ -12,11 +24,7 @@ module Zxcvbn
12
24
  pos = j
13
25
  j -= 1
14
26
 
15
- match = Match.new(
16
- i: i,
17
- j: j,
18
- token: password.slice(i, j - i + 1)
19
- )
27
+ match = MatchBuilder.new(i:, j:, token: password.slice(i, j - i + 1))
20
28
  yield match, re_match
21
29
  end
22
30
  end
@@ -1,30 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'zxcvbn/match'
3
+ require 'zxcvbn/match_builder'
4
4
 
5
5
  module Zxcvbn
6
6
  module Matchers
7
+ # Finds repeated substrings in a password (e.g. "abcabc", "aaaa").
8
+ #
9
+ # Uses a greedy/lazy regex disambiguation strategy from zxcvbn.js v4:
10
+ # prefer the greedier match unless the lazy match is longer, then use
11
+ # LAZY_ANCHORED to extract the minimal repeating unit (base_token).
12
+ # @api private
7
13
  class Repeat
14
+ # Greedily matches the longest repeated substring.
15
+ GREEDY = /(.+)\1+/
16
+ # Lazily matches the shortest repeated substring.
17
+ LAZY = /(.+?)\1+/
18
+ # Anchored lazy pattern used to extract the minimal base token.
19
+ LAZY_ANCHORED = /^(.+?)\1+$/
20
+
21
+ # Find all repeated-substring matches in the password.
22
+ #
23
+ # @param password [String] the password to search
24
+ # @return [Array<MatchBuilder>] matches with pattern 'repeat', each containing
25
+ # base_token (the repeated unit) and repeat_count
8
26
  def matches(password)
9
27
  result = []
10
- i = 0
11
- while i < password.length
12
- cur_char = password[i]
13
- j = i + 1
14
- j += 1 while cur_char == password[j]
15
-
16
- if j - i > 2 # don't consider length 1 or 2 chains.
17
- result << Match.new(
18
- pattern: 'repeat',
19
- i: i,
20
- j: j - 1,
21
- token: password.slice(i, j - i),
22
- repeated_char: cur_char
23
- )
28
+ last_index = 0
29
+
30
+ while last_index < password.length
31
+ greedy_match = GREEDY.match(password, last_index)
32
+ lazy_match = LAZY.match(password, last_index)
33
+ break unless greedy_match
34
+
35
+ if greedy_match[0].length > lazy_match[0].length
36
+ rx_match = greedy_match
37
+ base_token = LAZY_ANCHORED.match(rx_match[0])[1]
38
+ else
39
+ rx_match = lazy_match
40
+ base_token = rx_match[1]
24
41
  end
25
42
 
26
- i = j
43
+ i = rx_match.begin(0)
44
+ j = rx_match.end(0) - 1
45
+ token = rx_match[0]
46
+
47
+ result << MatchBuilder.new(
48
+ pattern: 'repeat',
49
+ i:,
50
+ j:,
51
+ token:,
52
+ base_token:,
53
+ repeat_count: token.length / base_token.length
54
+ )
55
+
56
+ last_index = j + 1
27
57
  end
58
+
28
59
  result
29
60
  end
30
61
  end
@@ -1,67 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'zxcvbn/match'
3
+ require 'zxcvbn/match_builder'
4
4
 
5
5
  module Zxcvbn
6
6
  module Matchers
7
+ # Matches monotonically incrementing or decrementing character sequences,
8
+ # such as "abcde", "54321", or "ZYXW".
9
+ # @api private
7
10
  class Sequences
8
- SEQUENCES = {
9
- 'lower' => 'abcdefghijklmnopqrstuvwxyz',
10
- 'upper' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
11
- 'digits' => '01234567890'
12
- }.freeze
11
+ # Maximum absolute step between adjacent characters for a valid sequence.
12
+ MAX_DELTA = 5
13
+ # Matches tokens that are all lowercase letters.
14
+ ALL_LOWER = /^[a-z]+$/
15
+ # Matches tokens that are all uppercase letters.
16
+ ALL_UPPER = /^[A-Z]+$/
17
+ # Matches tokens that are all digits.
18
+ ALL_DIGITS = /^\d+$/
13
19
 
14
- def seq_match_length(password, from, direction, seq)
15
- index_from = seq.index(password[from])
16
- j = 1
17
- while from + j < password.length &&
18
- password[from + j] == seq[index_from + direction * j]
19
- j += 1
20
- end
21
- j
22
- end
20
+ # @param password [String]
21
+ # @return [Array<MatchBuilder>] matches with pattern "sequence"
22
+ def matches(password)
23
+ return [] if password.length < 2
23
24
 
24
- # find the first matching sequence, and return with
25
- # direction, if characters are one apart in the sequence
26
- def applicable_sequence(password, i)
27
- SEQUENCES.each do |name, sequence|
28
- index1 = sequence.index(password[i])
29
- index2 = sequence.index(password[i + 1])
30
- next unless index1 && index2
25
+ result = []
26
+ start = 0
27
+ last_delta = nil
31
28
 
32
- seq_direction = index2 - index1
33
- return [name, sequence, seq_direction] if [-1, 1].include?(seq_direction)
29
+ emit = lambda do |seq_end, delta|
30
+ return if delta.nil?
34
31
 
35
- return nil
32
+ abs_delta = delta.abs
33
+ return unless abs_delta.positive? && abs_delta <= MAX_DELTA
34
+
35
+ len = seq_end - start + 1
36
+ return unless len > 2 || abs_delta == 1
37
+
38
+ token = password[start, len]
39
+ seq_name, seq_space = classify(token)
40
+ result << MatchBuilder.new(
41
+ pattern: 'sequence',
42
+ i: start,
43
+ j: seq_end,
44
+ token:,
45
+ sequence_name: seq_name,
46
+ sequence_space: seq_space,
47
+ ascending: delta.positive?
48
+ )
36
49
  end
37
- end
38
50
 
39
- def matches(password)
40
- result = []
41
- i = 0
42
- while i < password.length - 1
43
- seq_name, seq, seq_direction = applicable_sequence(password, i)
51
+ (1...password.length).each do |i|
52
+ delta = password[i].ord - password[i - 1].ord
53
+ last_delta = delta if last_delta.nil?
54
+ next if delta == last_delta
44
55
 
45
- if seq
46
- length = seq_match_length(password, i, seq_direction, seq)
47
- if length > 2
48
- result << Match.new(
49
- pattern: 'sequence',
50
- i: i,
51
- j: i + length - 1,
52
- token: password[i, length],
53
- sequence_name: seq_name,
54
- sequence_space: seq.length,
55
- ascending: seq_direction == 1
56
- )
57
- end
58
- i += length - 1
59
- else
60
- i += 1
61
- end
56
+ emit.call(i - 1, last_delta)
57
+ start = i - 1
58
+ last_delta = delta
62
59
  end
60
+ emit.call(password.length - 1, last_delta)
61
+
63
62
  result
64
63
  end
64
+
65
+ private
66
+
67
+ def classify(token)
68
+ case token
69
+ when ALL_LOWER then ['lower', 26]
70
+ when ALL_UPPER then ['upper', 26]
71
+ when ALL_DIGITS then ['digits', 10]
72
+ else ['unicode', 26]
73
+ end
74
+ end
65
75
  end
66
76
  end
67
77
  end
@@ -1,14 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'zxcvbn/match'
3
+ require 'zxcvbn/match_builder'
4
4
 
5
5
  module Zxcvbn
6
6
  module Matchers
7
+ # Matches keyboard spatial patterns (e.g. "qwerty", "asdf") across all
8
+ # configured adjacency graphs.
9
+ # @api private
7
10
  class Spatial
11
+ # Matches characters that require the Shift key on a standard keyboard.
12
+ SHIFTED_RX = /[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?]/
13
+
14
+ # @param graphs [Hash{String => Hash}] adjacency graph data keyed by graph name
8
15
  def initialize(graphs)
9
16
  @graphs = graphs
10
17
  end
11
18
 
19
+ # @param password [String]
20
+ # @return [Array<MatchBuilder>] matches with pattern "spatial" across all graphs
12
21
  def matches(password)
13
22
  results = []
14
23
  @graphs.each do |graph_name, graph|
@@ -17,14 +26,21 @@ module Zxcvbn
17
26
  results
18
27
  end
19
28
 
29
+ # Returns spatial matches found in password using a single adjacency graph.
30
+ #
31
+ # @param graph [Hash] adjacency map for each key character
32
+ # @param graph_name [String] name of the graph (e.g. "qwerty")
33
+ # @param password [String]
34
+ # @return [Array<MatchBuilder>] matches with pattern "spatial"
20
35
  def matches_for_graph(graph, graph_name, password)
21
36
  result = []
37
+ keyboard_graph = %w[qwerty dvorak].include?(graph_name)
22
38
  i = 0
23
39
  while i < password.length - 1
24
40
  j = i + 1
25
41
  last_direction = nil
26
42
  turns = 0
27
- shifted_count = 0
43
+ shifted_count = keyboard_graph && SHIFTED_RX.match?(password[i]) ? 1 : 0
28
44
  loop do
29
45
  prev_char = password[j - 1]
30
46
  found = false
@@ -60,14 +76,14 @@ module Zxcvbn
60
76
  else
61
77
  # otherwise push the pattern discovered so far, if any...
62
78
  if j - i > 2 # don't consider length 1 or 2 chains.
63
- result << Match.new(
79
+ result << MatchBuilder.new(
64
80
  pattern: 'spatial',
65
- i: i,
81
+ i:,
66
82
  j: j - 1,
67
83
  token: password.slice(i, j - i),
68
84
  graph: graph_name,
69
- turns: turns,
70
- shifted_count: shifted_count
85
+ turns:,
86
+ shifted_count:
71
87
  )
72
88
  end
73
89
  # ...and then start a new search for the rest of the password.
@@ -4,11 +4,16 @@ require 'zxcvbn/matchers/regex_helpers'
4
4
 
5
5
  module Zxcvbn
6
6
  module Matchers
7
+ # Matches 4-digit year substrings (1900–2019) in the password.
8
+ # @api private
7
9
  class Year
8
10
  include RegexHelpers
9
11
 
10
- YEAR_REGEX = /19\d\d|200\d|201\d/.freeze
12
+ # Matches years from 1900 to 2019, matching zxcvbn.js v4.
13
+ YEAR_REGEX = /19\d\d|200\d|201\d/
11
14
 
15
+ # @param password [String]
16
+ # @return [Array<MatchBuilder>] matches with pattern "year"
12
17
  def matches(password)
13
18
  result = []
14
19
  re_match_all(YEAR_REGEX, password) do |match|
data/lib/zxcvbn/math.rb CHANGED
@@ -1,35 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zxcvbn
4
+ # Mathematical utilities used by the guess estimation logic.
5
+ # @api private
4
6
  module Math
5
- def bruteforce_cardinality(password)
6
- is_type_of = {}
7
-
8
- password.each_byte do |ordinal|
9
- case ordinal
10
- when (48..57)
11
- is_type_of['digits'] = true
12
- when (65..90)
13
- is_type_of['upper'] = true
14
- when (97..122)
15
- is_type_of['lower'] = true
16
- else
17
- is_type_of['symbols'] = true
18
- end
19
- end
20
-
21
- cardinality = 0
22
- cardinality += 10 if is_type_of['digits']
23
- cardinality += 26 if is_type_of['upper']
24
- cardinality += 26 if is_type_of['lower']
25
- cardinality += 33 if is_type_of['symbols']
26
- cardinality
27
- end
28
-
29
- def lg(n)
30
- ::Math.log(n, 2)
31
- end
32
-
7
+ # Computes the binomial coefficient C(n, k) ("n choose k").
8
+ #
9
+ # @param n [Integer]
10
+ # @param k [Integer]
11
+ # @return [Integer]
33
12
  def nCk(n, k)
34
13
  return 0 if k > n
35
14
  return 1 if k.zero?
@@ -47,10 +26,18 @@ module Zxcvbn
47
26
  r
48
27
  end
49
28
 
29
+ # Returns the precomputed average key-adjacency degree for a keyboard graph.
30
+ #
31
+ # @param graph_name [String] e.g. "qwerty" or "keypad"
32
+ # @return [Float]
50
33
  def average_degree_for_graph(graph_name)
51
34
  data.graph_stats[graph_name][:average_degree]
52
35
  end
53
36
 
37
+ # Returns the number of starting positions (keys) in a keyboard graph.
38
+ #
39
+ # @param graph_name [String] e.g. "qwerty" or "keypad"
40
+ # @return [Integer]
54
41
  def starting_positions_for_graph(graph_name)
55
42
  data.graph_stats[graph_name][:starting_positions]
56
43
  end