lib/zxcvbn/matching.rb in zxcvbn-0.1.9 vs lib/zxcvbn/matching.rb in zxcvbn-0.1.10

- old
+ new

@@ -126,81 +126,87 @@ end # ------------------------------------------------------------------------------ # omnimatch -- combine everything ---------------------------------------------- # ------------------------------------------------------------------------------ - def self.omnimatch(password) + def self.omnimatch(password, user_inputs = []) + user_dict = build_user_input_dictionary(user_inputs) matches = [] - matchers = [ - :dictionary_match, - :reverse_dictionary_match, - :l33t_match, - :spatial_match, - :repeat_match, - :sequence_match, - :regex_match, - :date_match - ] - matchers.each do |matcher| - matches += send(matcher, password) - end + matches += dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES) + matches += reverse_dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES) + matches += l33t_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES, _l33t_table = L33T_TABLE) + matches += spatial_match(password, _graphs = GRAPHS) + matches += repeat_match(password, user_dict) + matches += sequence_match(password) + matches += regex_match(password, _regexen = REGEXEN) + matches += date_match(password) sorted(matches) end #------------------------------------------------------------------------------- # dictionary match (common passwords, english, last names, etc) ---------------- #------------------------------------------------------------------------------- - def self.dictionary_match(password, _ranked_dictionaries = RANKED_DICTIONARIES) + def self.dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES) # _ranked_dictionaries variable is for unit testing purposes matches = [] + _ranked_dictionaries.each do |dictionary_name, ranked_dict| + check_dictionary(matches, password, dictionary_name, ranked_dict) + end + check_dictionary(matches, password, "user_inputs", user_dict) + sorted(matches) + end + + def self.check_dictionary(matches, password, dictionary_name, ranked_dict) len = password.length password_lower = password.downcase - _ranked_dictionaries.each do |dictionary_name, ranked_dict| - longest_dict_word_size = RANKED_DICTIONARIES_MAX_WORD_SIZE.fetch(dictionary_name) do - ranked_dict.keys.max_by(&:size)&.size || 0 - end - search_width = [longest_dict_word_size, len].min - (0...len).each do |i| - search_end = [i + search_width, len].min - (i...search_end).each do |j| - if ranked_dict.key?(password_lower[i..j]) - word = password_lower[i..j] - rank = ranked_dict[word] - matches << { - "pattern" => "dictionary", - "i" => i, - "j" => j, - "token" => password[i..j], - "matched_word" => word, - "rank" => rank, - "dictionary_name" => dictionary_name, - "reversed" => false, - "l33t" => false - } - end + longest_word_size = RANKED_DICTIONARIES_MAX_WORD_SIZE.fetch(dictionary_name) do + ranked_dict.keys.max_by(&:size)&.size || 0 + end + search_width = [longest_word_size, len].min + (0...len).each do |i| + search_end = [i + search_width, len].min + (i...search_end).each do |j| + if ranked_dict.key?(password_lower[i..j]) + word = password_lower[i..j] + rank = ranked_dict[word] + matches << { + "pattern" => "dictionary", + "i" => i, + "j" => j, + "token" => password[i..j], + "matched_word" => word, + "rank" => rank, + "dictionary_name" => dictionary_name, + "reversed" => false, + "l33t" => false + } end end end - sorted(matches) end - def self.reverse_dictionary_match(password, _ranked_dictionaries = RANKED_DICTIONARIES) + def self.reverse_dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES) reversed_password = password.reverse - matches = dictionary_match(reversed_password, _ranked_dictionaries) + matches = dictionary_match(reversed_password, user_dict, _ranked_dictionaries) matches.each do |match| match["token"] = match["token"].reverse match["reversed"] = true # map coordinates back to original string match["i"], match["j"] = [password.length - 1 - match["j"], password.length - 1 - match["i"]] end sorted(matches) end - def self.user_input_dictionary=(ordered_list) - ranked_dict = build_ranked_dict(ordered_list.dup) - RANKED_DICTIONARIES["user_inputs"] = ranked_dict - RANKED_DICTIONARIES_MAX_WORD_SIZE["user_inputs"] = ranked_dict.keys.max_by(&:size)&.size || 0 + def self.build_user_input_dictionary(user_inputs_or_dict) + # optimization: if we receive a hash, we've been given the dict back (from the repeat matcher) + return user_inputs_or_dict if user_inputs_or_dict.is_a?(Hash) + + sanitized_inputs = [] + user_inputs_or_dict.each do |arg| + sanitized_inputs << arg.to_s.downcase if arg.is_a?(String) || arg.is_a?(Numeric) || arg == true || arg == false + end + build_ranked_dict(sanitized_inputs) end #------------------------------------------------------------------------------- # dictionary match with common l33t substitutions ------------------------------ #------------------------------------------------------------------------------- @@ -285,17 +291,17 @@ end sub_dicts end - def self.l33t_match(password, _ranked_dictionaries = RANKED_DICTIONARIES, _l33t_table = L33T_TABLE) + def self.l33t_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES, _l33t_table = L33T_TABLE) matches = [] enumerate_l33t_subs(relevant_l33t_subtable(password, _l33t_table)).each do |sub| break if sub.empty? # corner case: password has no relevant subs. subbed_password = translate(password, sub) - dictionary_match(subbed_password, _ranked_dictionaries).each do |match| + dictionary_match(subbed_password, user_dict, _ranked_dictionaries).each do |match| token = password[match["i"]..match["j"]] if token.downcase == match["matched_word"] next # only return the matches that contain an actual substitution end @@ -401,11 +407,11 @@ end #------------------------------------------------------------------------------- # repeats (aaa, abcabcabc) and sequences (abcdef) ------------------------------ #------------------------------------------------------------------------------- - def self.repeat_match(password) + def self.repeat_match(password, user_dict) matches = [] greedy = /(.+)\1+/ lazy = /(.+?)\1+/ lazy_anchored = /^(.+?)\1+$/ last_index = 0 @@ -434,10 +440,10 @@ base_token = match[1] end i = match.begin(0) j = match.end(0) - 1 # recursively match and score the base string - base_analysis = Scoring.most_guessable_match_sequence(base_token, omnimatch(base_token)) + base_analysis = Scoring.most_guessable_match_sequence(base_token, omnimatch(base_token, user_dict)) base_matches = base_analysis["sequence"] base_guesses = base_analysis["guesses"] matches << { "pattern" => "repeat", "i" => i,