zxcvbn 0.1.0 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,44 +6,40 @@ module Zxcvbn
6
6
  # this calculates the average over all keys.
7
7
  def self.calc_average_degree(graph)
8
8
  average = 0
9
- graph.each do |key, neighbors|
10
- average += neighbors.count {|n| n }
9
+ graph.each do |_key, neighbors|
10
+ average += neighbors.count { |n| n }.to_f
11
11
  end
12
- average /= graph.keys.size
13
- return average
12
+ average /= graph.keys.size.to_f
13
+ average
14
14
  end
15
15
 
16
16
  BRUTEFORCE_CARDINALITY = 10
17
17
 
18
- MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10000
18
+ MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10_000
19
19
 
20
20
  MIN_SUBMATCH_GUESSES_SINGLE_CHAR = 10
21
21
 
22
22
  MIN_SUBMATCH_GUESSES_MULTI_CHAR = 50
23
23
 
24
- def self.nCk(n, k)
24
+ def self.nck(n, k) # rubocop:disable Naming/MethodParameterName
25
25
  # http://blog.plover.com/math/choose.html
26
- if k > n
27
- return 0
28
- end
29
- if k == 0
30
- return 1
31
- end
32
- r = 1
26
+ return 0.0 if k > n
27
+ return 1.0 if k == 0
28
+
29
+ r = 1.0
33
30
  (1..k).each do |d|
34
31
  r *= n
35
32
  r /= d
36
- n -= 1
33
+ n -= 1.0
37
34
  end
38
- return r
35
+ r
39
36
  end
40
37
 
41
- def self.factorial(n)
38
+ def self.factorial(n) # rubocop:disable Naming/MethodParameterName
42
39
  # unoptimized, called only on small n
43
- if n < 2
44
- return 1
45
- end
46
- return (2..10).reduce(&:*)
40
+ return 1 if n < 2
41
+
42
+ (2..n).reduce(&:*)
47
43
  end
48
44
 
49
45
  # ------------------------------------------------------------------------------
@@ -78,17 +74,17 @@ module Zxcvbn
78
74
  # D^(l-1) approximates Sum(D^i for i in [1..l-1]
79
75
 
80
76
  # ------------------------------------------------------------------------------
81
- def self.most_guessable_match_sequence(password, matches, _exclude_additive = false)
77
+ def self.most_guessable_match_sequence(password, matches, _exclude_additive: false)
82
78
  n = password.length
83
79
  # partition matches into sublists according to ending index j
84
80
  matches_by_j = (0...n).map { [] }
85
81
  matches.each do |m|
86
- matches_by_j[m[:j]] << m
82
+ matches_by_j[m["j"]] << m
87
83
  end
88
84
 
89
85
  # small detail: for deterministic output, sort each sublist by i.
90
86
  matches_by_j.each do |lst|
91
- lst.sort_by!{|m| m[:i] }
87
+ lst.sort_by! { |m| m["i"] }
92
88
  end
93
89
 
94
90
  optimal = {
@@ -96,59 +92,57 @@ module Zxcvbn
96
92
  # password prefix up to k, inclusive.
97
93
  # if there is no length-l sequence that scores better (fewer guesses) than
98
94
  # a shorter match sequence spanning the same prefix, optimal.m[k][l] is undefined.
99
- m: (0...n).map { {} },
95
+ "m" => (0...n).map { {} },
100
96
  # same structure as optimal.m -- holds the product term Prod(m.guesses for m in sequence).
101
97
  # optimal.pi allows for fast (non-looping) updates to the minimization function.
102
- pi: (0...n).map { {} },
98
+ "pi" => (0...n).map { {} },
103
99
  # same structure as optimal.m -- holds the overall metric.
104
- g: (0...n).map { {} },
100
+ "g" => (0...n).map { {} }
105
101
  }
106
102
 
107
103
  # helper: considers whether a length-l sequence ending at match m is better (fewer guesses)
108
104
  # than previously encountered sequences, updating state if so.
109
- update = -> (m, l) do
110
- k = m[:j]
105
+ update = lambda do |m, l|
106
+ k = m["j"]
111
107
  pi = estimate_guesses(m, password)
112
108
  if l > 1
113
109
  # we're considering a length-l sequence ending with match m:
114
110
  # obtain the product term in the minimization function by multiplying m's guesses
115
111
  # by the product of the length-(l-1) sequence ending just before m, at m.i - 1.
116
- pi *= optimal[:pi][m[:i] - 1][l - 1]
112
+ pi *= optimal["pi"][m["i"] - 1][l - 1]
117
113
  end
118
114
  # calculate the minimization func
119
115
  g = factorial(l) * pi
120
- if !_exclude_additive
121
- g += MIN_GUESSES_BEFORE_GROWING_SEQUENCE ** (l - 1)
122
- end
116
+ g += MIN_GUESSES_BEFORE_GROWING_SEQUENCE**(l - 1) if !_exclude_additive
123
117
  # update state if new best.
124
118
  # first see if any competing sequences covering this prefix, with l or fewer matches,
125
119
  # fare better than this sequence. if so, skip it and return.
126
- optimal[:g][k].each do |competing_l, competing_g|
127
- if competing_l > l
128
- next
129
- end
130
- if competing_g <= g
131
- return
132
- end
120
+ optimal["g"][k].find do |competing_l, competing_g|
121
+ next if competing_l > l
122
+ return nil if competing_g <= g
133
123
  end
134
124
  # this sequence might be part of the final optimal sequence.
135
- optimal[:g][k][l] = g
136
- optimal[:m][k][l] = m
137
- optimal[:pi][k][l] = pi
125
+ optimal["g"][k][l] = g
126
+ optimal["m"][k][l] = m
127
+ optimal["pi"][k][l] = pi
128
+
129
+ optimal["g"][k] = optimal["g"][k].sort.to_h
130
+ optimal["m"][k] = optimal["m"][k].sort.to_h
131
+ optimal["pi"][k] = optimal["pi"][k].sort.to_h
138
132
  end
139
133
 
140
134
  # helper: make bruteforce match objects spanning i to j, inclusive.
141
- make_bruteforce_match = -> (i, j) do
135
+ make_bruteforce_match = lambda do |i, j|
142
136
  return {
143
- pattern: 'bruteforce',
144
- token: password[i..j],
145
- i: i,
146
- j: j
137
+ "pattern" => "bruteforce",
138
+ "token" => password[i..j],
139
+ "i" => i,
140
+ "j" => j
147
141
  }
148
142
  end
149
143
 
150
144
  # helper: evaluate bruteforce matches ending at k.
151
- bruteforce_update = -> (k) do
145
+ bruteforce_update = lambda do |k|
152
146
  # see if a single bruteforce match spanning the k-prefix is optimal.
153
147
  m = make_bruteforce_match.call(0, k)
154
148
  update.call(m, 1)
@@ -156,16 +150,14 @@ module Zxcvbn
156
150
  # generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k).
157
151
  # see if adding these new matches to any of the sequences in optimal[i-1]
158
152
  # leads to new bests.
159
- m = make_bruteforce_match.call(i, k);
160
- optimal[:m][i-1].each do |l, last_m|
161
- l = l.to_i
153
+ m = make_bruteforce_match.call(i, k)
154
+ optimal["m"][i - 1].each do |l, last_m|
162
155
  # corner: an optimal sequence will never have two adjacent bruteforce matches.
163
156
  # it is strictly better to have a single bruteforce match spanning the same region:
164
157
  # same contribution to the guess product with a lower length.
165
158
  # --> safe to skip those cases.
166
- if last_m[:pattern] == 'bruteforce'
167
- next
168
- end
159
+ next if last_m["pattern"] == "bruteforce"
160
+
169
161
  # try adding m to this length-l sequence.
170
162
  update.call(m, l + 1)
171
163
  end
@@ -174,15 +166,15 @@ module Zxcvbn
174
166
 
175
167
  # helper: step backwards through optimal.m starting at the end,
176
168
  # constructing the final optimal match sequence.
177
- unwind = -> (n) do
169
+ unwind = lambda do |n2|
178
170
  optimal_match_sequence = []
179
- k = n - 1
171
+ k = n2 - 1
180
172
  # find the final best sequence length and score
181
- l, g = optimal[:g][k].min_by{|candidate_l, candidate_g| candidate_g }
173
+ l, _g = (optimal["g"][k] || []).min_by { |_candidate_l, candidate_g| candidate_g || 0 }
182
174
  while k >= 0
183
- m = optimal[:m][k][l]
175
+ m = optimal["m"][k][l]
184
176
  optimal_match_sequence.unshift(m)
185
- k = m[:i] - 1
177
+ k = m["i"] - 1
186
178
  l -= 1
187
179
  end
188
180
  return optimal_match_sequence
@@ -190,8 +182,8 @@ module Zxcvbn
190
182
 
191
183
  (0...n).each do |k|
192
184
  matches_by_j[k].each do |m|
193
- if m[:i] > 0
194
- optimal[:m][m[:i] - 1].keys.each do |l|
185
+ if m["i"] > 0
186
+ optimal["m"][m["i"] - 1].each_key do |l|
195
187
  update.call(m, l + 1)
196
188
  end
197
189
  else
@@ -205,18 +197,18 @@ module Zxcvbn
205
197
  optimal_l = optimal_match_sequence.length
206
198
 
207
199
  # corner: empty password
208
- if password.length == 0
209
- guesses = 1
200
+ guesses = if password.empty?
201
+ 1
210
202
  else
211
- guesses = optimal[:g][n - 1][optimal_l]
203
+ optimal["g"][n - 1][optimal_l]
212
204
  end
213
205
 
214
206
  # final result object
215
- return {
216
- password: password,
217
- guesses: guesses,
218
- guesses_log10: Math.log10(guesses),
219
- sequence: optimal_match_sequence
207
+ {
208
+ "password" => password,
209
+ "guesses" => guesses,
210
+ "guesses_log10" => Math.log10(guesses),
211
+ "sequence" => optimal_match_sequence
220
212
  }
221
213
  end
222
214
 
@@ -224,76 +216,73 @@ module Zxcvbn
224
216
  # guess estimation -- one function per match pattern ---------------------------
225
217
  # ------------------------------------------------------------------------------
226
218
  def self.estimate_guesses(match, password)
227
- if match[:guesses]
228
- return match[:guesses] # a match's guess estimate doesn't change. cache it.
219
+ if match["guesses"]
220
+ return match["guesses"] # a match's guess estimate doesn't change. cache it.
229
221
  end
222
+
230
223
  min_guesses = 1
231
- if match[:token].length < password.length
232
- min_guesses = if match[:token].length == 1
224
+ if match["token"].length < password.length
225
+ min_guesses = if match["token"].length == 1
233
226
  MIN_SUBMATCH_GUESSES_SINGLE_CHAR
234
227
  else
235
228
  MIN_SUBMATCH_GUESSES_MULTI_CHAR
236
229
  end
237
230
  end
238
231
  estimation_functions = {
239
- bruteforce: method(:bruteforce_guesses),
240
- dictionary: method(:dictionary_guesses),
241
- spatial: method(:spatial_guesses),
242
- repeat: method(:repeat_guesses),
243
- sequence: method(:sequence_guesses),
244
- regex: method(:regex_guesses),
245
- date: method(:date_guesses),
232
+ "bruteforce" => method(:bruteforce_guesses),
233
+ "dictionary" => method(:dictionary_guesses),
234
+ "spatial" => method(:spatial_guesses),
235
+ "repeat" => method(:repeat_guesses),
236
+ "sequence" => method(:sequence_guesses),
237
+ "regex" => method(:regex_guesses),
238
+ "date" => method(:date_guesses)
246
239
  }
247
- guesses = estimation_functions[match[:pattern].to_sym].call(match)
248
- match[:guesses] = [guesses, min_guesses].max;
249
- match[:guesses_log10] = Math.log10(match[:guesses])
250
- return match[:guesses]
240
+ guesses = estimation_functions[match["pattern"]].call(match)
241
+ match["guesses"] = [guesses, min_guesses].max
242
+ match["guesses_log10"] = Math.log10(match["guesses"])
243
+ match["guesses"]
251
244
  end
252
245
 
253
- MAX_VALUE = 2 ** 1024
246
+ MAX_VALUE = 2**1024
254
247
 
255
248
  def self.bruteforce_guesses(match)
256
- guesses = BRUTEFORCE_CARDINALITY ** match[:token].length
249
+ guesses = BRUTEFORCE_CARDINALITY**match["token"].length
257
250
  # trying to match JS behaviour here setting a MAX_VALUE to try to acheieve same values as JS library.
258
- if guesses > MAX_VALUE
259
- guesses = MAX_VALUE
260
- end
251
+ guesses = MAX_VALUE if guesses > MAX_VALUE
261
252
 
262
253
  # small detail: make bruteforce matches at minimum one guess bigger than smallest allowed
263
254
  # submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence.
264
- min_guesses = if match[:token].length == 1
255
+ min_guesses = if match["token"].length == 1
265
256
  MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1
266
257
  else
267
258
  MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1
268
259
  end
269
260
 
270
- [guesses, min_guesses].max
261
+ [guesses, min_guesses].max.to_f
271
262
  end
272
263
 
273
264
  def self.repeat_guesses(match)
274
- return match[:base_guesses] * match[:repeat_count]
265
+ match["base_guesses"] * match["repeat_count"]
275
266
  end
276
267
 
277
268
  def self.sequence_guesses(match)
278
- first_chr = match[:token][0]
269
+ first_chr = match["token"][0]
279
270
  # lower guesses for obvious starting points
280
- if ['a', 'A', 'z', 'Z', '0', '1', '9'].include?(first_chr)
281
- base_guesses = 4
271
+ base_guesses = if ["a", "A", "z", "Z", "0", "1", "9"].include?(first_chr)
272
+ 4
273
+ elsif first_chr.match?(/\d/)
274
+ 10
282
275
  else
283
- if first_chr.match?(/\d/)
284
- base_guesses = 10 # digits
285
- else
286
- # could give a higher base for uppercase,
287
- # assigning 26 to both upper and lower sequences is more conservative.
288
- base_guesses = 26
289
- end
276
+ # could give a higher base for uppercase,
277
+ # assigning 26 to both upper and lower sequences is more conservative.
278
+ 26
290
279
  end
291
- if !match[:ascending]
280
+ if !match["ascending"]
292
281
  # need to try a descending sequence in addition to every ascending sequence ->
293
282
  # 2x guesses
294
283
  base_guesses *= 2
295
284
  end
296
- return base_guesses * match[:token].length
285
+ base_guesses * match["token"].length
297
286
  end
298
287
 
299
288
  MIN_YEAR_SPACE = 20
@@ -301,127 +290,124 @@ module Zxcvbn
301
290
 
302
291
  def self.regex_guesses(match)
303
292
  char_class_bases = {
304
- alpha_lower: 26,
305
- alpha_upper: 26,
306
- alpha: 52,
307
- alphanumeric: 62,
308
- digits: 10,
309
- symbols: 33
293
+ "alpha_lower" => 26,
294
+ "alpha_upper" => 26,
295
+ "alpha" => 52,
296
+ "alphanumeric" => 62,
297
+ "digits" => 10,
298
+ "symbols" => 33
310
299
  }
311
- if char_class_bases.has_key? match[:regex_name]
312
- return char_class_bases[match[:regex_name]] ** match[:token].length
313
- elsif match[:regex_name] == 'recent_year'
300
+ if char_class_bases.key? match["regex_name"]
301
+ char_class_bases[match["regex_name"]]**match["token"].length
302
+ elsif match["regex_name"] == "recent_year"
314
303
  # conservative estimate of year space: num years from REFERENCE_YEAR.
315
304
  # if year is close to REFERENCE_YEAR, estimate a year space of MIN_YEAR_SPACE.
316
- year_space = abs(match[:regex_match[0]].to_i - REFERENCE_YEAR).abs
317
- year_space = [year_space, MIN_YEAR_SPACE].max
318
- return year_space
305
+ year_space = (match["regex_match"][0].to_i - REFERENCE_YEAR).abs
306
+ [year_space, MIN_YEAR_SPACE].max
307
+
319
308
  end
320
309
  end
321
310
 
322
311
  def self.date_guesses(match)
323
312
  # base guesses: (year distance from REFERENCE_YEAR) * num_days * num_years
324
- year_space = [(match[:year] - REFERENCE_YEAR).abs, MIN_YEAR_SPACE].max
313
+ year_space = [(match["year"] - REFERENCE_YEAR).abs, MIN_YEAR_SPACE].max
325
314
  guesses = year_space * 365
326
- if match[:separator]
315
+ separator = match["separator"]
316
+ if !["", nil].include?(separator)
327
317
  # add factor of 4 for separator selection (one of ~4 choices)
328
318
  guesses *= 4
329
319
  end
330
- return guesses
320
+ guesses
331
321
  end
332
322
 
333
- KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS[:qwerty])
323
+ KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["qwerty"]).freeze
334
324
  # slightly different for keypad/mac keypad, but close enough
335
- KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS[:keypad])
325
+ KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["keypad"]).freeze
336
326
 
337
- KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS[:qwerty].keys.size
338
- KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS[:keypad].keys.size
327
+ KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS["qwerty"].keys.size
328
+ KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS["keypad"].keys.size
339
329
 
340
330
  def self.spatial_guesses(match)
341
- if ['qwerty', 'dvorak'].include?(match[:graph])
342
- s = KEYBOARD_STARTING_POSITIONS;
343
- d = KEYBOARD_AVERAGE_DEGREE;
331
+ if ["qwerty", "dvorak"].include?(match["graph"])
332
+ s = KEYBOARD_STARTING_POSITIONS
333
+ d = KEYBOARD_AVERAGE_DEGREE
344
334
  else
345
- s = KEYPAD_STARTING_POSITIONS;
346
- d = KEYPAD_AVERAGE_DEGREE;
335
+ s = KEYPAD_STARTING_POSITIONS
336
+ d = KEYPAD_AVERAGE_DEGREE
347
337
  end
348
- guesses = 0
349
- ll = match[:token].length
350
- t = match[:turns]
338
+ guesses = 0.0
339
+ ll = match["token"].length
340
+ t = match["turns"]
351
341
  # estimate the number of possible patterns w/ length ll or less with t turns or less.
352
342
  (2..ll).each do |i|
353
343
  possible_turns = [t, i - 1].min
354
344
  (1..possible_turns).each do |j|
355
- guesses += nCk(i - 1, j - 1) * s * (d ** j)
345
+ guesses += nck((i - 1).to_f, (j - 1).to_f) * s.to_f * (d.to_f**j.to_f)
356
346
  end
357
347
  end
358
348
  # add extra guesses for shifted keys. (% instead of 5, A instead of a.)
359
349
  # math is similar to extra guesses of l33t substitutions in dictionary matches.
360
- if match[:shifted_count]
361
- ss = match[:shifted_count]
362
- uu = match[:token].length - match[:shifted_count] # unshifted count
350
+ if match["shifted_count"] && match["shifted_count"] != 0
351
+ ss = match["shifted_count"]
352
+ uu = match["token"].length - match["shifted_count"] # unshifted count
363
353
  if ss == 0 || uu == 0
364
354
  guesses *= 2
365
355
  else
366
356
  shifted_variations = 0
367
357
  (1..[ss, uu].min).each do |i|
368
- shifted_variations += nCk(ss + uu, i)
358
+ shifted_variations += nck((ss + uu).to_f, i.to_f)
369
359
  end
370
360
  guesses *= shifted_variations
371
361
  end
372
362
  end
373
- return guesses
363
+ guesses
374
364
  end
375
365
 
376
366
  def self.dictionary_guesses(match)
377
- match[:base_guesses] = match[:rank] # keep these as properties for display purposes
378
- match[:uppercase_variations] = uppercase_variations(match)
379
- match[:l33t_variations] = l33t_variations(match)
380
- reversed_variations = match[:reversed] && 2 || 1
381
- return match[:base_guesses] * match[:uppercase_variations] * match[:l33t_variations] * reversed_variations
367
+ match["base_guesses"] = match["rank"] # keep these as properties for display purposes
368
+ match["uppercase_variations"] = uppercase_variations(match)
369
+ match["l33t_variations"] = l33t_variations(match)
370
+ reversed_variations = match["reversed"] && 2 || 1
371
+ match["base_guesses"] * match["uppercase_variations"] * match["l33t_variations"] * reversed_variations
382
372
  end
383
373
 
384
- START_UPPER = /^[A-Z][^A-Z]+$/
385
- END_UPPER = /^[^A-Z]+[A-Z]$/
386
- ALL_UPPER = /^[^a-z]+$/
387
- ALL_LOWER = /^[^A-Z]+$/
374
+ START_UPPER = /^[A-Z][^A-Z]+$/.freeze
375
+ END_UPPER = /^[^A-Z]+[A-Z]$/.freeze
376
+ ALL_UPPER = /^[^a-z]+$/.freeze
377
+ ALL_LOWER = /^[^A-Z]+$/.freeze
388
378
 
389
379
  def self.uppercase_variations(match)
390
- word = match[:token]
391
- if word.match?(ALL_LOWER) || word.downcase === word
392
- return 1
393
- end
380
+ word = match["token"]
381
+ return 1 if word.match?(ALL_LOWER) || word.downcase == word
382
+
394
383
  # a capitalized word is the most common capitalization scheme,
395
384
  # so it only doubles the search space (uncapitalized + capitalized).
396
385
  # allcaps and end-capitalized are common enough too, underestimate as 2x factor to be safe.
397
386
  [START_UPPER, END_UPPER, ALL_UPPER].each do |regex|
398
- if word.match?(regex)
399
- return 2
400
- end
387
+ return 2 if word.match?(regex)
401
388
  end
402
389
  # otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters
403
390
  # with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD),
404
391
  # the number of ways to lowercase U+L letters with L lowercase letters or less.
405
- uu = word.split("").count{|chr| chr.match?(/[A-Z]/)}
406
- ll = word.split("").count{|chr| chr.match?(/[a-z]/)}
407
- variations = 0;
392
+ uu = word.chars.count { |chr| chr.match?(/[A-Z]/) }
393
+ ll = word.chars.count { |chr| chr.match?(/[a-z]/) }
394
+ variations = 0
408
395
  (1..[uu, ll].min).each do |i|
409
- variations += nCk(uu + ll, i)
396
+ variations += nck(uu + ll, i)
410
397
  end
411
- return variations
398
+ variations
412
399
  end
413
400
 
414
401
  def self.l33t_variations(match)
415
- if !match[:l33t]
416
- return 1
417
- end
402
+ return 1 if !match["l33t"]
403
+
418
404
  variations = 1
419
- match[:sub].each do |subbed, unsubbed|
405
+ match["sub"].each do |subbed, unsubbed|
420
406
  # lower-case match.token before calculating: capitalization shouldn't affect l33t calc.
421
- chrs = match[:token].downcase.split('')
422
- ss = chrs.count{|chr| chr == subbed }
423
- uu = chrs.count{|chr| chr == unsubbed }
424
- if ss === 0 || uu === 0
407
+ chrs = match["token"].downcase.chars
408
+ ss = chrs.count { |chr| chr == subbed }
409
+ uu = chrs.count { |chr| chr == unsubbed }
410
+ if ss == 0 || uu == 0
425
411
  # for this sub, password is either fully subbed (444) or fully unsubbed (aaa)
426
412
  # treat that as doubling the space (attacker needs to try fully subbed chars in addition to
427
413
  # unsubbed.)
@@ -432,12 +418,12 @@ module Zxcvbn
432
418
  p = [uu, ss].min
433
419
  possibilities = 0
434
420
  (1..p).each do |i|
435
- possibilities += nCk(uu + ss, i)
421
+ possibilities += nck(uu + ss, i)
436
422
  end
437
423
  variations *= possibilities
438
424
  end
439
425
  end
440
- return variations
426
+ variations
441
427
  end
442
428
  end
443
429
  end