alexpass 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/alexpass.rb ADDED
@@ -0,0 +1,91 @@
1
+ class Alexpass
2
+
3
+ VERSION = '0.1.0'
4
+
5
+ DEFAULT_LENGTH = 8
6
+ DEFAULT_OPTIONS = {:length => DEFAULT_LENGTH, :memorizable => true}
7
+
8
+ # character sets divided by touch typing hands
9
+ LN = '12345'.split('') # Left-hand Numbers
10
+ RN = '67890' .split('') # Right ''
11
+ LL = ('qwert'+'asdfg'+'zxcvb').split('') # Left-hand Lowercase letters
12
+ RL = ('yuiop'+'hjkl'+'nm').split('') # Right '' ''
13
+ LU = LL.collect {|c| c.capitalize} # Left-hand Uppercase letters
14
+ RU = RL.collect {|c| c.capitalize} # Right '' ''
15
+
16
+ AMBIGUOUS = '1lI0O' + # visually ambiguous
17
+ '6b' # left/right ambiguous
18
+
19
+ # patterns for even and odd length passwords;
20
+ # alternating hands, with the last always being on the left;
21
+ # only the first in each subset takes part in the "memorizable" pattern: LUUNLLNL
22
+ @pattern_even = [[RL], [LU, LN, LL], [RU, RN, RL], [LN, LL, LU], [RL, RU, RN], [LL, LU, LN], [RN, RL, RU], [LL]]
23
+
24
+ @pattern_odd = [[LL], [RU, RN, RL], [LU, LN, LL], [RN, RL, RU], [LL, LU, LN], [RL, RU, RN], [LN, LL, LU], [RL]]
25
+
26
+ # return a password
27
+ def self.generate(options={})
28
+ options = self._verify_options(options)
29
+ password = ''
30
+ iterations = options[:length]/DEFAULT_LENGTH + 1
31
+ iterations.times { password += self._generate(options) }
32
+ password.slice(0...options[:length])
33
+ end
34
+
35
+ # return the number of permutations,
36
+ # and additionally print out details when options[:permutations] contains v's
37
+ def self.permutations(options={})
38
+ options = self._verify_options(options)
39
+ p = 1
40
+ pattern = options[:length].even? ? @pattern_even : @pattern_odd
41
+ plen = pattern.length
42
+ samples = []
43
+ (0...options[:length]).each { |i|
44
+ samples[i] = (options[:memorizable] ? pattern[i%plen][0] : pattern[i%plen].flatten).reject{|c| AMBIGUOUS.include?(c)}
45
+ p *= samples[i].length # take the product of all sample sizes to get the total number of permutations
46
+ }
47
+ if options[:permutations] =~ /^v{3,}\Z/
48
+ puts "sample sets:"
49
+ samples.each {|s|
50
+ puts s.inspect
51
+ }
52
+ end
53
+ puts "#{samples.collect {|s| s.length}.join(' * ')} permutations" if options[:permutations] =~ /^v{2,}\Z/
54
+ p
55
+ end
56
+
57
+ private
58
+
59
+ # verify and return an options hash, after merging the default options with the incoming ones
60
+ def self._verify_options(options=nil)
61
+ raise ArgumentError, "expected a Hash, but got: #{options.inspect}" unless options.kind_of?(Hash)
62
+ options = DEFAULT_OPTIONS.merge(options)
63
+ raise ArgumentError, "expected :memorizable to be boolean, but got: #{options[:memorizable].inspect}" unless options[:memorizable].kind_of?(TrueClass) || options[:memorizable].kind_of?(FalseClass)
64
+ raise ArgumentError, "expected :length to be a Fixnum, but got: #{options[:length].inspect}" unless options[:length].kind_of?(Fixnum)
65
+ raise ArgumentError, "expected :length > 0, but got: #{options[:length]}" unless options[:length] > 0
66
+ options
67
+ end
68
+
69
+ # generate and return a string, having a parity-appropriate pattern, where each character is sampled from the appropriate character set (depending on whether :memorizable is true or false)
70
+ def self._generate(options={})
71
+ options = self._verify_options(options)
72
+ pattern = options[:length].even? ? @pattern_even : @pattern_odd
73
+ return pattern.collect { |i|
74
+ s =''
75
+ loop do
76
+ s = _sample(i[options[:memorizable] ? 0 : rand(i.length)])
77
+ break unless AMBIGUOUS.include?(s)
78
+ end
79
+ s
80
+ }.join.slice(0..-1)
81
+ end
82
+
83
+ # pick a random element from the given array
84
+ def self._sample(a=[])
85
+ raise ArgumentError, "expected an Array, but got: #{a.class}" unless a.kind_of?(Array)
86
+ raise ArgumentError, "expected a non-empty Array, but got an empty one" unless a.length > 0
87
+ a[rand(a.length)]
88
+ end
89
+
90
+ end
91
+
@@ -0,0 +1,65 @@
1
+ # encoding: utf-8
2
+ require 'test_helper'
3
+
4
+ class AlexpassTest < Test::Unit::TestCase
5
+
6
+ def test__generate__default_length
7
+ assert_equal Alexpass::DEFAULT_LENGTH, Alexpass.generate.length
8
+ end
9
+
10
+ def test__generate__random_length
11
+ random_length = rand(20)+1
12
+ assert_equal random_length, Alexpass.generate(:length=>random_length).length
13
+ end
14
+
15
+ def test__generate__raise_exception_for_non_hash_argument
16
+ non_hash_argument = 10
17
+ exception = assert_raise(ArgumentError) { Alexpass.generate(non_hash_argument) }
18
+ assert_equal "expected a Hash, but got: #{non_hash_argument}", exception.message
19
+ end
20
+
21
+ def test__generate__raise_exception_for_bad_memorizable_hash_value_type
22
+ non_boolean = {:memorizable => 10}
23
+ exception = assert_raise(ArgumentError) { Alexpass.generate(non_boolean) }
24
+ assert_equal "expected :memorizable to be boolean, but got: #{non_boolean[:memorizable].inspect}", exception.message
25
+ end
26
+
27
+ def test__generate__raise_exception_for_bad_length_hash_value_type
28
+ non_fixnum = {:length => 'A'}
29
+ exception = assert_raise(ArgumentError) { Alexpass.generate(non_fixnum) }
30
+ assert_equal "expected :length to be a Fixnum, but got: #{non_fixnum[:length].inspect}", exception.message
31
+ end
32
+
33
+ def test_raise_exception_for_fixnum_argument_less_than_one_to_generate_to_length
34
+ fixnum_argument = {:length => 0}
35
+ exception = assert_raise(ArgumentError) { Alexpass.generate(fixnum_argument) }
36
+ assert_equal "expected :length > 0, but got: #{fixnum_argument[:length].inspect}", exception.message
37
+ end
38
+
39
+ def test__sample
40
+ assert_equal 'a', Alexpass._sample(['a'])
41
+ assert_equal 'a', Alexpass._sample(['a', 'a', 'a'])
42
+ random_element = Alexpass._sample(['a', 'b', 'c'])
43
+ assert_equal String, random_element.class
44
+ assert_equal 1, random_element.length
45
+ end
46
+
47
+ def test__sample_raise_exception_for_non_array_argument
48
+ exception = assert_raise(ArgumentError) { Alexpass._sample({}) }
49
+ assert_equal "expected an Array, but got: Hash", exception.message
50
+ end
51
+
52
+ def test__sample_raise_exception_for_empty_array_argument
53
+ exception = assert_raise(ArgumentError) { Alexpass._sample([]) }
54
+ assert_equal "expected a non-empty Array, but got an empty one", exception.message
55
+ end
56
+
57
+ def test__permutations
58
+ # these hardcoded values will need to be updated if/when the character set lengths change
59
+ assert_equal 31752000, Alexpass.permutations
60
+ assert_equal 53572004640, Alexpass.permutations(:memorizable => false)
61
+ assert_equal 60011280000, Alexpass.permutations(:memorizable => true, :length => 11)
62
+ assert_equal 544505855160960, Alexpass.permutations(:memorizable => false, :length => 11)
63
+ end
64
+
65
+ end
@@ -0,0 +1,4 @@
1
+ require 'test/unit'
2
+ #require 'alexpass'
3
+ require './lib/alexpass.rb'
4
+
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alexpass
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Batko
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-10 00:00:00.000000000Z
13
+ dependencies: []
14
+ description: Generate passwords derived from hand-alternating, visually unambiguous,
15
+ alphanumeric characters.
16
+ email:
17
+ - alexbatko@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/alexpass.rb
23
+ - test/test_alexpass.rb
24
+ - test/test_helper.rb
25
+ homepage: https://github.com/abatko/alexpass
26
+ licenses:
27
+ - MIT
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 1.8.10
47
+ signing_key:
48
+ specification_version: 3
49
+ summary: Generate passwords derived from hand-alternating, visually unambiguous, alphanumeric
50
+ characters
51
+ test_files:
52
+ - test/test_alexpass.rb
53
+ - test/test_helper.rb