lib/zxcvbn/matching.rb in zxcvbn-0.1.1 vs lib/zxcvbn/matching.rb in zxcvbn-0.1.2

- old
+ new

@@ -14,14 +14,14 @@ RANKED_DICTIONARIES = FREQUENCY_LISTS.each_with_object({}) do |(name, lst), o| o[name] = build_ranked_dict(lst); end GRAPHS = { - qwerty: ADJACENCY_GRAPHS[:qwerty], - dvorak: ADJACENCY_GRAPHS[:dvorak], - keypad: ADJACENCY_GRAPHS[:keypad], - mac_keypad: ADJACENCY_GRAPHS[:mac_keypad] + "qwerty" => ADJACENCY_GRAPHS["qwerty"], + "dvorak" => ADJACENCY_GRAPHS["dvorak"], + "keypad" => ADJACENCY_GRAPHS["keypad"], + "mac_keypad" => ADJACENCY_GRAPHS["mac_keypad"] } L33T_TABLE = { "a" => ['4', '@'], "b" => ['8'], @@ -38,11 +38,11 @@ } REGEXEN = { # alpha_lower: /[a-z]/, # recent_year: /19\d\d|200\d|201\d/g - recent_year: /19\d\d|200\d|201\d/ + "recent_year" => /19\d\d|200\d|201\d/ } DATE_MAX_YEAR = 2050 DATE_MIN_YEAR = 1000 @@ -120,21 +120,21 @@ string.split('').map {|chr| chr_map[chr] || chr}.join("") end def self.sorted(matches) # sort on i primary, j secondary - matches.sort_by{|match| [match[:i], match[:j]] } + matches.sort_by!{|match| [match["i"], match["j"]] } end # ------------------------------------------------------------------------------ # omnimatch -- combine everything ---------------------------------------------- # ------------------------------------------------------------------------------ def self.omnimatch(password) matches = [] - matchers = [method(:dictionary_match), method(:reverse_dictionary_match), method(:l33t_match), method(:spatial_match), method(:repeat_match), method(:sequence_match), method(:regex_match), method(:date_match)] + matchers = [:dictionary_match, :reverse_dictionary_match, :l33t_match, :spatial_match, :repeat_match, :sequence_match, :regex_match, :date_match] matchers.each do |matcher| - matches += matcher.call(password) + matches += send(matcher, password) end return sorted(matches) end #------------------------------------------------------------------------------- @@ -150,19 +150,19 @@ (i...len).each do |j| if ranked_dict.has_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.to_s, - reversed: false, - l33t: false + "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 end @@ -171,14 +171,14 @@ def self.reverse_dictionary_match(password, _ranked_dictionaries = RANKED_DICTIONARIES) reversed_password = password.reverse matches = dictionary_match(reversed_password, _ranked_dictionaries) matches.each do |match| - match[:token] = match[:token].reverse - match[:reversed] = true + 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]] + match["i"], match["j"] = [password.length - 1 - match["j"], password.length - 1 - match["i"]] end return sorted(matches) end def self.set_user_input_dictionary(ordered_list) @@ -282,32 +282,32 @@ if empty(sub) # corner case: password has no relevant subs. break end subbed_password = translate(password, sub) dictionary_match(subbed_password, _ranked_dictionaries).each do |match| - token = password[match[:i]..match[:j]] - if token.downcase == match[:matched_word] + token = password[match["i"]..match["j"]] + if token.downcase == match["matched_word"] next # only return the matches that contain an actual substitution end match_sub = {} # subset of mappings in sub that are in use for this match sub.each do |subbed_chr, chr| if token.index(subbed_chr) match_sub[subbed_chr] = chr end end - match[:l33t] = true; - match[:token] = token; - match[:sub] = match_sub; - match[:sub_display] = results = match_sub.map {|k, v| "#{k} -> #{v}" }.join(", ") + match["l33t"] = true; + match["token"] = token; + match["sub"] = match_sub; + match["sub_display"] = results = match_sub.map {|k, v| "#{k} -> #{v}" }.join(", ") matches << match end end return sorted(matches.select do |match| # filter single-character l33t matches to reduce noise. # otherwise '1' matches 'i', '4' matches 'a', both very common English words # with low dictionary rank. - match[:token].length > 1; + match["token"].length > 1; end) end # ------------------------------------------------------------------------------ # spatial match (qwerty/dvorak/keypad) ----------------------------------------- @@ -322,15 +322,16 @@ SHIFTED_RX = /[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?]/ def self.spatial_match_helper(password, graph, graph_name) matches = [] - (0...password.length).each do |i| + i = 0 + while i < password.length - 1 j = i + 1 last_direction = nil turns = 0 - if (graph_name == :qwerty || graph_name == :dvorak) && SHIFTED_RX.match?(password[i]) + if (graph_name == 'qwerty' || graph_name == 'dvorak') && SHIFTED_RX.match?(password[i]) # initial character is shifted shifted_count = 1 else shifted_count = 0 end @@ -370,17 +371,17 @@ j += 1 else # otherwise push the pattern discovered so far, if any... if j - i > 2 # don't consider length 1 or 2 chains. matches << { - pattern: 'spatial', - i: i, - j: j - 1, - token: password[i..j], - graph: graph_name.to_s, - turns: turns, - shifted_count: shifted_count + "pattern" => 'spatial', + "i" => i, + "j" => j - 1, + "token" => password[i...j], + "graph" => graph_name, + "turns" => turns, + "shifted_count" => shifted_count } end # ...and then start a new search for the rest of the password. i = j break @@ -404,10 +405,11 @@ greedy_match = greedy.match(password, last_index) lazy_match = lazy.match(password, last_index) if !greedy_match break end + # coverage ??? if (greedy_match[0].length > lazy_match[0].length) # greedy beats lazy for 'aabaab' # greedy: [aabaab, aab] # lazy: [aa, a] match = greedy_match @@ -424,21 +426,21 @@ base_token = match[1]; end i, j = [match.begin(0), match.end(0) - 1] # recursively match and score the base string base_analysis = Scoring.most_guessable_match_sequence(base_token, omnimatch(base_token)) - base_matches = base_analysis[:sequence] - base_guesses = base_analysis[:guesses] + base_matches = base_analysis["sequence"] + base_guesses = base_analysis["guesses"] matches << { - pattern: 'repeat', - i: i, - j: j, - token: match[0], - base_token: base_token, - base_guesses: base_guesses, - base_matches: base_matches, - repeat_count: match[0].length / base_token.length + "pattern" => 'repeat', + "i" => i, + "j" => j, + "token" => match[0], + "base_token" => base_token, + "base_guesses" => base_guesses, + "base_matches" => base_matches, + "repeat_count" => match[0].length / base_token.length } last_index = j + 1 end return matches end @@ -482,30 +484,29 @@ # (this could be improved) sequence_name = 'unicode' sequence_space = 26 end return result << { - pattern: 'sequence', - i: i, - j: j, - token: password[i..j], - sequence_name: sequence_name, - sequence_space: sequence_space, - ascending: delta > 0 + "pattern" => 'sequence', + "i" =>i, + "j" =>j, + "token" => password[i..j], + "sequence_name" => sequence_name, + "sequence_space" => sequence_space, + "ascending" => delta > 0 } end end end result = [] i = 0 last_delta = nil + (1...password.length).each do |k| - delta = password.bytes[k] - password.bytes[k - 1] - if !last_delta - last_delta = delta - end + delta = password[k].ord - password[k - 1].ord + last_delta ||= delta if delta == last_delta next end j = k - 1 update.call(i, j, last_delta) @@ -525,16 +526,16 @@ # regex.lastIndex = 0; # keeps regex_match stateless match_index = 0 while rx_match = regex.match(password, match_index) token = rx_match[0] matches << { - pattern: 'regex', - token: token, - i: rx_match.begin(0), - j: rx_match.end(0) - 1, - regex_name: name.to_s, - regex_match: rx_match + "pattern" => 'regex', + "token" => token, + "i" => rx_match.begin(0), + "j" => rx_match.end(0) - 1, + "regex_name" => name, + "regex_match" => rx_match.to_a } match_index = rx_match.begin(0) + 1 end end return sorted(matches) @@ -600,20 +601,20 @@ # match the candidate date that likely takes the fewest guesses: a year closest to 2000. # (scoring.REFERENCE_YEAR). # ie, considering '111504', prefer 11-15-04 to 1-1-1504 # (interpreting '04' as 2004) - best_candidate = candidates.min_by{|candidate| (candidate[:year] - Scoring::REFERENCE_YEAR).abs } + best_candidate = candidates.min_by{|candidate| (candidate["year"] - Scoring::REFERENCE_YEAR).abs } matches << { - pattern: 'date', - token: token, - i: i, - j: j, - separator: '', - year: best_candidate[:year], - month: best_candidate[:month], - day: best_candidate[:day] + "pattern" => 'date', + "token" => token, + "i" => i, + "j" => j, + "separator" => '', + "year" => best_candidate["year"], + "month" => best_candidate["month"], + "day" => best_candidate["day"] } end end # dates with separators are between length 6 '1/1/91' and 10 '11/11/1991' (0..password.length - 6).each do |i| @@ -629,31 +630,32 @@ dmy = map_ints_to_dmy([rx_match[1].to_i, rx_match[3].to_i, rx_match[4].to_i]) if !dmy next end matches << { - pattern: 'date', - token: token, - i: i, - j: j, - separator: rx_match[2], - year: dmy[:year], - month: dmy[:month], - day: dmy[:day] + "pattern" => 'date', + "token" => token, + "i" => i, + "j" => j, + "separator" => rx_match[2], + "year" => dmy["year"], + "month" => dmy["month"], + "day" => dmy["day"] } end end # matches now contains all valid date strings in a way that is tricky to capture # with regexes only. while thorough, it will contain some unintuitive noise: # '2015_06_04', in addition to matching 2015_06_04, will also contain # 5(!) other date matches: 15_06_04, 5_06_04, ..., even 2015 (matched as 5/1/2020) # to reduce noise, remove date matches that are strict substrings of others - return sorted(matches.uniq.select do |match| + return sorted(matches.uniq.reject do |match| matches.find do |other_match| - other_match[:i] <= match[:i] && other_match[:j] >= match[:j] + (match["i"] > other_match["i"] && match["j"] <= other_match["j"]) || + (match["i"] >= other_match["i"] && match["j"] < other_match["j"]) end end) end def self.map_ints_to_dmy(ints) @@ -696,13 +698,13 @@ possible_year_splits.each do |(y, rest)| if DATE_MIN_YEAR <= y && y <= DATE_MAX_YEAR dm = map_ints_to_dm(rest) if dm return { - year: y, - month: dm[:month], - day: dm[:day] + "year" => y, + "month" => dm["month"], + "day" => dm["day"] } else # for a candidate that includes a four-digit year, # when the remaining ints don't match to a day and month, # it is not a date. @@ -716,23 +718,23 @@ possible_year_splits.each do |(y, rest)| dm = map_ints_to_dm(rest) if dm y = two_to_four_digit_year(y) return { - year: y, - month: dm[:month], - day: dm[:day] + "year" => y, + "month" => dm["month"], + "day" => dm["day"] } end end end def self.map_ints_to_dm(ints) [ints, ints.reverse].each do |(d, m)| if (1 <= d && d <= 31) && (1 <= m && m <= 12) return { - day: d, - month: m + "day" => d, + "month" => m } end end return nil end