pog19 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,415 @@
1
+ ##
2
+ # = password_tests.rb
3
+ #
4
+ # Copyright (c) 2007 Operis Systems, LLC
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ ##
20
+ # = PasswordTests Class
21
+ #
22
+ # PasswordTests provides functionality for testing strength of passwords
23
+ #
24
+ # === Usage
25
+ #
26
+ # 1. Pass the constructor either a Password object or a String (or anything
27
+ # that can be converted to a String using +to_s+).
28
+ # 2. Pass the constructor a Hash of tests and their corresponding parameters.
29
+ # The tests are method names of the PasswordTests object.
30
+ # 3. Execute the +run+ method to execute the tests.
31
+ # 4. Inspect the returned +Hash+ to verify pass/fail for each test.
32
+ #
33
+ # === Examples
34
+ #
35
+ # Test the length of the password
36
+ #
37
+ # tests = PasswordTests.new( Password.new('foobar'), :test_length => 8)
38
+ # results = tests.run => {:test_length => false}
39
+ #
40
+ # Test the length of the password and for at least 2 upper case alpha
41
+ # characters.
42
+ #
43
+ # tests = PasswordTests.new( 'FooBar', :test_length => 8,
44
+ # :test_minimum_alphas => 2 )
45
+ # results = tests.run => {:test_length => false,
46
+ # :test_minimum_alphas => false}
47
+ #
48
+ # === Tests
49
+ #
50
+ # These are the currently implemented tests; however, feel free to mix-in your
51
+ # own or extend the PasswordTests class.
52
+ #
53
+ # test_length:: minimum length
54
+ # test_minimum_alphas:: minimum number of alpha characters (upper
55
+ # and lower case)
56
+ # test_maximum_alphas:: maximum number of alpha characters (upper
57
+ # and lower case)
58
+ # test_minimum_upper_alphas:: minimum number of uppercase alpha
59
+ # characters
60
+ # test_maximum_upper_alphas:: maximum number of uppercase alpha
61
+ # characters
62
+ # test_minimum_lower_alphas:: minimum number of lowercase alpha
63
+ # characters
64
+ # test_maximum_lower_alphas:: maximum number of lowercase alpha
65
+ # characters
66
+ # test_minimum_numerals:: minimum number of numeric characters
67
+ # test_maximum_numerals:: maximum number of numeric characters
68
+ # test_minimum_non_alphanumeric:: minimum number of non-alphanumeric
69
+ # characters (symbols)
70
+ # test_maximum_non_alphanumeric:: maximum number of non-alphanumeric
71
+ # characters (symbols)
72
+ #
73
+ # === Calculating the qualitative strength of a password
74
+ #
75
+ # PasswordTests.qualitative_strength( '123456') => 0
76
+ # PasswordTests.qualitative_strength( 'foo' ) => 0
77
+ # PasswordTests.qualitative_strength( 'fooo' ) => 2
78
+ # PasswordTests.qualitative_strength( 'foobar' ) => 12
79
+ # PasswordTests.qualitative_strength( '1fo0^*bar9' ) => 95
80
+ #
81
+ #
82
+ # === Calculating the entropy of a password
83
+ #
84
+ # Password.entropy('1') => 3.32192809488736
85
+ # Password.entropy('123456') => 19.9315685693242
86
+ # Password.entropy('foobar') => 28.2026383088466
87
+ # Password.entropy('1fo0^*bar9') => 61.0852445677817
88
+
89
+ class PasswordTests
90
+ ##
91
+ # Initialize the password tests (in +test_params+) with the given +password+.
92
+ # If +test_params+ is left out or is an empty Hash, the medium-low default will
93
+ # be used from PasswordTests.default_test_params
94
+ #
95
+ # Example: The following would run the test_length and test_minimum_alphas
96
+ # tests (and both would fail on the given password)
97
+ #
98
+ # tests = PasswordTests.new( 'FooBar', :test_length => 8,
99
+ # :test_minimum_alphas => 2 )
100
+ #
101
+ # Example: The following would run the high default level tests (and it would
102
+ # fail on the given password)
103
+ #
104
+ # tests = PasswordTests.new( 'FooBar', :high )
105
+ #
106
+ def initialize( password, test_params = {} )
107
+ @password = password.to_s
108
+ if test_params.is_a?(Symbol)
109
+ @test_params = default_test_params(test_params)
110
+ elsif test_params.empty?
111
+ @test_params = default_test_params
112
+ else
113
+ @test_params = test_params
114
+ end
115
+ end
116
+
117
+ ##
118
+ # Run the tests.
119
+ #
120
+ # The results are returned in a Hash with the key as the test, and the value
121
+ # as whether or not the test passed
122
+ #
123
+ # Example:
124
+ #
125
+ # tests = PasswordTests.new( 'FooBar', :test_length => 8,
126
+ # :test_minimum_alphas => 2 )
127
+ # results = tests.run => {:test_length => false,
128
+ # :test_minimum_alphas => false}
129
+ #
130
+ def run
131
+ results = Hash.new { |h,k| h[k] = false }
132
+ @test_params.each { |test,param| results[test] = send(test, param) }
133
+ return results
134
+ end
135
+
136
+ ##
137
+ # Tests the length of the password
138
+ #
139
+ def test_length( *params )
140
+ params[0] ||= @test_params[current_method_name]
141
+ @password.length >= params[0]
142
+ end
143
+
144
+ ##
145
+ # Tests for a minimum number of alpha characters
146
+ #
147
+ def test_minimum_alphas( *params )
148
+ params[0] ||= @test_params[current_method_name]
149
+ minimum_test( /[A-Za-z]/, params[0] )
150
+ end
151
+
152
+ ##
153
+ # Tests for a maximum number of alpha characters
154
+ #
155
+ def test_maximum_alphas( *params )
156
+ params[0] ||= @test_params[current_method_name]
157
+ maximum_test( /[A-Za-z]/, params[0] )
158
+ end
159
+
160
+ ##
161
+ # Tests for a minimum number of upper case alpha characters
162
+ #
163
+ def test_minimum_upper_alphas( *params )
164
+ params[0] ||= @test_params[current_method_name]
165
+ minimum_test( /[A-Z]/, params[0] )
166
+ end
167
+
168
+ ##
169
+ # Tests for a maximum number of upper case alpha characters
170
+ #
171
+ def test_maximum_upper_alphas( *params )
172
+ params[0] ||= @test_params[current_method_name]
173
+ maximum_test( /[A-Z]/, params[0] )
174
+ end
175
+
176
+ ##
177
+ # Tests for a minimum number of lower case alpha characters
178
+ #
179
+ def test_minimum_lower_alphas( *params )
180
+ params[0] ||= @test_params[current_method_name]
181
+ minimum_test( /[a-z]/, params[0] )
182
+ end
183
+
184
+ ##
185
+ # Tests for a maximum number of lower case alpha characters
186
+ #
187
+ def test_maximum_lower_alphas( *params )
188
+ params[0] ||= @test_params[current_method_name]
189
+ maximum_test( /[a-z]/, params[0] )
190
+ end
191
+
192
+ ##
193
+ # Tests for a minimum number of numeric characters
194
+ #
195
+ def test_minimum_numerals( *params )
196
+ params[0] ||= @test_params[current_method_name]
197
+ minimum_test( /[0-9]/, params[0] )
198
+ end
199
+
200
+ ##
201
+ # Tests for a maximum number of numeric characters
202
+ #
203
+ def test_maximum_numerals( *params )
204
+ params[0] ||= @test_params[current_method_name]
205
+ maximum_test( /[0-9]/, params[0] )
206
+ end
207
+
208
+ ##
209
+ # Tests for a minimum number of non-alpha (i.e. not letters) characters
210
+ #
211
+ def test_minimum_non_alpha( *params )
212
+ params[0] ||= @test_params[current_method_name]
213
+ minimum_test( /[^A-Za-z]/, params[0] )
214
+ end
215
+
216
+ ##
217
+ # Tests for a maximum number of non-alpha (i.e. not letters) characters
218
+ #
219
+ def test_maximum_non_alpha( *params )
220
+ params[0] ||= @test_params[current_method_name]
221
+ maximum_test( /[^A-Za-z]/, params[0] )
222
+ end
223
+
224
+ ##
225
+ # Tests for a minimum number of non-alphanumeric characters
226
+ #
227
+ def test_minimum_non_alphanumeric( *params )
228
+ params[0] ||= @test_params[current_method_name]
229
+ minimum_test( /[^A-Za-z0-9]/, params[0] )
230
+ end
231
+
232
+ ##
233
+ # Tests for a maximum number of non-alphanumeric characters
234
+ #
235
+ def test_maximum_non_alphanumeric( *params )
236
+ params[0] ||= @test_params[current_method_name]
237
+ maximum_test( /[^A-Za-z0-9]/, params[0] )
238
+ end
239
+
240
+ ##
241
+ # Get a default test parameters hash of the given level of security. The
242
+ # default level is <tt>:medium_low</tt>.
243
+ #
244
+ # Levels:
245
+ #
246
+ # <tt>:low</tt>: minimum 6 characters, at least 1 letter
247
+ # <tt>:medium_low</tt>: minimum 8 characters, at least 1 letter and 1
248
+ # non-alpha character (ie. number or symbol) <b>default</b>
249
+ # <tt>:medium_high</tt>: minimum 8 characters, at least 1 upper-case letter,
250
+ # 1 lower-case letter, and 1 non-alpha character (ie.
251
+ # number or symbol)
252
+ # <tt>:high</tt>: minimum 8 characters, at least 1 upper-case letter,
253
+ # 1 lower-case letter, 1 numeral character, and 1
254
+ # symbol
255
+ #
256
+ def default_test_params( level = :medium_low )
257
+ case level
258
+ when :low
259
+ { :test_length => 6,
260
+ :test_minimum_alphas => 1 }
261
+ when :medium_low
262
+ { :test_length => 8,
263
+ :test_minimum_alphas => 1,
264
+ :test_minimum_non_alpha => 1}
265
+ when :medium_high
266
+ { :test_length => 8,
267
+ :test_minimum_upper_alphas => 1,
268
+ :test_minimum_lower_alphas => 1,
269
+ :test_minimum_non_alpha => 1 }
270
+ when :high
271
+ { :test_length => 8,
272
+ :test_minimum_upper_alphas => 1,
273
+ :test_minimum_lower_alphas => 1,
274
+ :test_minimum_numerals => 1,
275
+ :test_minimum_non_alphanumeric => 1}
276
+ else
277
+ raise "#{level.to_s} is not a valid level"
278
+ end
279
+ end
280
+
281
+ ##
282
+ # Tests the strength of a given password qualitativly and returns a numerical
283
+ # strength.
284
+ #
285
+ # The algorithm is modified from
286
+ # http://phiras.wordpress.com/2007/04/08/password-strength-meter-a-jquery-plugin/
287
+ #
288
+ # Examples:
289
+ #
290
+ # PasswordTests.qualitative_strength( '123456') => 0
291
+ # PasswordTests.qualitative_strength( 'foo' ) => 0
292
+ # PasswordTests.qualitative_strength( 'fooo' ) => 2
293
+ # PasswordTests.qualitative_strength( 'foobar' ) => 12
294
+ # PasswordTests.qualitative_strength( '1fo0^*bar9' ) => 95
295
+ #
296
+ def self.qualitative_strength( password )
297
+ return 0 if password.length < 4
298
+
299
+ score = password.length * 4
300
+
301
+ # repetitions
302
+ reps = PasswordTests.repetitions( password )
303
+ reps.each { |rep,times| score -= rep.size * 2 * times }
304
+
305
+ # counts
306
+ nums = password.scan( /\d/ ).size
307
+ symbols = password.scan( /[^A-Za-z0-9]/ ).size
308
+ lower_alphas = password.scan( /[a-z]/ ).size
309
+ upper_alphas = password.scan( /[A-Z]/ ).size
310
+
311
+ alphas = lower_alphas + upper_alphas
312
+
313
+ score += 5 if nums >= 3
314
+ score += 5 if symbols >= 2
315
+ score += 15 if nums > 0 && symbols > 0
316
+ score -= 10 if nums == 0 && symbols == 0
317
+ score += 10 if lower_alphas > 0 && upper_alphas > 0
318
+ score += 15 if nums > 0 && alphas > 0
319
+ score += 15 if symbols > 0 && alphas > 0
320
+ score -= 30 if symbols == 0 && alphas == 0
321
+
322
+ return score if score > 0
323
+ return 0
324
+ end
325
+
326
+ ##
327
+ # Finds all character sequences that are repeated in the password
328
+ #
329
+ # Example:
330
+ #
331
+ # PasswordTests.repetitions( 'foofoo') => {"foo" => 1, "f" => 1, "oo" => 1, "o" => 3}
332
+ #
333
+ def self.repetitions( password, splits = [], reps = {} )
334
+ splits = [password[0...(password.length/2)],
335
+ password[(password.length/2)...password.length]] if splits.empty?
336
+
337
+ next_splits = []
338
+
339
+ splits.each do |partial|
340
+ r = password.scan(partial)
341
+ reps[r[0]] = r.size-1 if r.size > 1 && (not reps.include?(r[0]))
342
+
343
+ if partial.length > 1
344
+ next_splits << partial[0...(partial.length/2)]
345
+ next_splits << partial[(partial.length/2)...partial.length]
346
+ end
347
+ end
348
+
349
+ repetitions( password, next_splits, reps ) unless next_splits.empty?
350
+
351
+ return reps
352
+ end
353
+
354
+ ##
355
+ # Calculates the entropy of the given password (in bits).
356
+ #
357
+ # Note: Technically, when using a deterministic (pseudo) random number
358
+ # generator, the entropy of ANY password will be the size of the set of
359
+ # integers the random function is picking from (eg. if you use rand(255) the
360
+ # entropy would be 8, but if you use rand(), the entropy would be 32).
361
+ #
362
+ # This is a simplified implementation of the formula for entropy on page 4
363
+ # of RFC 4086 (http://www.ietf.org/rfc/rfc4086.txt).
364
+ #
365
+ # This wikipedia has an explanation of password entropy:
366
+ # http://en.wikipedia.org/wiki/Random_password_generator
367
+ #
368
+ # Examples:
369
+ #
370
+ # Password.entropy('1') => 3.32192809488736
371
+ # Password.entropy('123456') => 19.9315685693242
372
+ # Password.entropy('foobar') => 28.2026383088466
373
+ # Password.entropy('1fo0^*bar9') => 61.0852445677817
374
+ #
375
+ def self.entropy( password )
376
+ n = 0
377
+ n += 26 if password.scan( /[a-z]/ ).size > 0
378
+ n += 26 if password.scan( /[A-Z]/ ).size > 0
379
+ n += 10 if password.scan( /[0-9]/ ).size > 0
380
+ n += 33 if password.scan( /[^A-Za-z0-9]/ ).size > 0
381
+
382
+ return password.length * (Math.log10(n) / Math.log10(2))
383
+ end
384
+ protected
385
+
386
+ ##
387
+ # Execute a minimum test
388
+ #
389
+ def minimum_test( pattern, minimum )
390
+ @password.scan(pattern).size >= minimum
391
+ end
392
+
393
+ ##
394
+ # Execute a maximum test
395
+ #
396
+ def maximum_test( pattern, maximum )
397
+ @password.scan(pattern).size <= maximum
398
+ end
399
+
400
+ ##
401
+ # Returns the method name of the method that calls current_method_name
402
+ #
403
+ # Example:
404
+ #
405
+ # def test
406
+ # current_method_name
407
+ # end
408
+ #
409
+ # irb(main):063:0> test
410
+ # => :test
411
+ #
412
+ def current_method_name
413
+ caller[0].match(/`([^']+)/).captures[0].to_sym
414
+ end
415
+ end
data/lib/pog.rb ADDED
@@ -0,0 +1,21 @@
1
+ ##
2
+ # = pog.rb
3
+ #
4
+ # Copyright (c) 2007 Operis Systems, LLC
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ require 'rubygems'
19
+
20
+ require File.join( File.dirname(__FILE__), 'password.rb' )
21
+ require File.join( File.dirname(__FILE__), 'password_tests.rb' )