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