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.
- 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' )
|