pog19 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +45 -0
- data/LICENSE +201 -0
- data/NOTICE +12 -0
- data/README +66 -0
- data/Rakefile +121 -0
- data/SECURITY +36 -0
- data/VERSION +1 -0
- data/lib/character_ranges.rb +185 -0
- data/lib/encoding_translation.rb +53 -0
- data/lib/password.rb +329 -0
- data/lib/password_tests.rb +415 -0
- data/lib/pog.rb +21 -0
- data/lib/random_string_generator.rb +291 -0
- data/lib/salt.rb +135 -0
- data/lib/string.rb +15 -0
- data/pog1-9.gemspec +20 -0
- data/test/tc_password.rb +122 -0
- data/test/tc_password_tests.rb +209 -0
- data/test/tc_random_string_generator.rb +133 -0
- data/test/tc_salt.rb +78 -0
- metadata +84 -0
@@ -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' )
|