pog19 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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' )