keybox 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ module Keybox
2
+ module Cipher
3
+ AES_256 = "aes-256-cbc"
4
+ DEFAULT_ALGORITHM = AES_256
5
+ end
6
+ end
@@ -0,0 +1 @@
1
+ require 'keybox/convert/csv'
@@ -0,0 +1,96 @@
1
+
2
+ require 'csv'
3
+ require 'keybox/entry'
4
+ module Keybox
5
+ module Convert
6
+ #
7
+ # Convert to/from a CSV file. When loading a CSV file, it is assumed
8
+ # that there is a header on the csv file that shows which of the fields
9
+ # belongs to the various fields of the entry. At a minimum the header
10
+ # line should have the default fields from an HostAccountEntry which are:
11
+ #
12
+ # - title
13
+ # - username
14
+ # - hostname
15
+ # - password
16
+ # - additional_info
17
+ #
18
+ # These headers can be in any order and CSV will do the right
19
+ # thing. But these headers must exist. A
20
+ # Keybox::ValidationError is thrown if they do not.
21
+ #
22
+ class CSV
23
+ class << self
24
+
25
+ # parse the header line from the CSV file and make sure
26
+ # that all the required columns are listed.
27
+ #
28
+ def parse_header(header)
29
+ field_indexes = {}
30
+ Keybox::HostAccountEntry.default_fields.each do |field|
31
+ field_indexes[field] = header.index(field)
32
+ if field_indexes[field].nil? then
33
+ raise Keybox::ValidationError, "There must be a header on the CSV to import and it must contain the '#{field}' field."
34
+ end
35
+ end
36
+ field_indexes
37
+ end
38
+
39
+ # returns an Array of AccountEntry classes or its
40
+ # descendants
41
+ #
42
+ def from_file(csv_filename)
43
+ reader = ::CSV.open(csv_filename,"r")
44
+ entries = Keybox::Convert::CSV.from_reader(reader)
45
+ reader.close
46
+ return entries
47
+ end
48
+
49
+ # pull all the items from the CSV file. There MUST be a
50
+ # header line that says what the different fields are.
51
+ # A HostAccountEntry object is created for each line and
52
+ # the array of those objects is returned
53
+ #
54
+ def from_reader(csv_reader)
55
+ field_indexes = parse_header(csv_reader.shift)
56
+ entries = []
57
+ csv_reader.each do |row|
58
+ entry = Keybox::HostAccountEntry.new
59
+ field_indexes.each_pair do |field,index|
60
+ value = row[index] || ""
61
+ entry.send("#{field}=",value.strip)
62
+ end
63
+ entries << entry
64
+ end
65
+ return entries
66
+ end
67
+
68
+ #
69
+ # records should be an array of AccountEntry objects
70
+ #
71
+ def to_file(records,csv_filename)
72
+ writer = ::CSV.open(csv_filename,"w")
73
+ Keybox::Convert::CSV.to_writer(records,writer)
74
+ writer.close
75
+ end
76
+
77
+ #
78
+ # write all the fields for each record. We go through
79
+ # all the records (an array of AccountEntry objects),
80
+ # recording all the fields, then using that as the header.
81
+ #
82
+ def to_writer(records,csv_writer)
83
+ field_names = records.collect { |r| r.fields }.flatten.uniq
84
+ csv_writer << field_names
85
+ records.each do |record|
86
+ values = []
87
+ field_names.each do |field|
88
+ values << record.send(field) || ""
89
+ end
90
+ csv_writer << values
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,13 @@
1
+ require 'openssl'
2
+ module Keybox
3
+ #
4
+ # By default Keybox wants to use sha-256 as the hashing function.
5
+ # This is available in OpenSSL 0.9.8 or greater.
6
+ #
7
+ module Digest
8
+ SHA_256 = "sha256"
9
+ CLASSES = { SHA_256 => ::OpenSSL::Digest::SHA256 }
10
+ DEFAULT_ALGORITHM = SHA_256
11
+ ALGORITHMS = CLASSES.keys
12
+ end
13
+ end
@@ -0,0 +1,200 @@
1
+ require 'keybox/storage/record'
2
+
3
+ module Keybox
4
+
5
+ # Entries in the Keybox storage container. The base class is
6
+ # AccountEntry with current child classes HostAccountEntry and
7
+ # URLAccountEntry.
8
+ #
9
+ # In most cases HostAccountEntry will suffice. Use URLAccountEntry
10
+ # when you would like to link an entry's +password+ field to be
11
+ # generated based upon the master password of the container.
12
+ class AccountEntry < Keybox::Storage::Record
13
+ class << self
14
+ def default_fields
15
+ %w(title username additional_info)
16
+ end
17
+
18
+ # fields that can be displayed, some could be calculated
19
+ def display_fields
20
+ (visible_fields + private_fields).uniq
21
+ end
22
+
23
+ def private_fields
24
+ []
25
+ end
26
+
27
+ def visible_fields
28
+ default_fields - private_fields
29
+ end
30
+
31
+ def visible_field?(field_name)
32
+ visible_fields.include?(field_name)
33
+ end
34
+
35
+ def private_field?(field_name)
36
+ private_fields.include?(field_name)
37
+ end
38
+ end
39
+
40
+ def each
41
+ fields.each do |f|
42
+ yield [f,self.send(f)]
43
+ end
44
+ end
45
+
46
+ # fields that are actually stored in the entry
47
+ def fields
48
+ (default_fields + @data_members.keys ).collect { |k| k.to_s }.uniq
49
+ end
50
+
51
+ def default_fields
52
+ self.class.default_fields
53
+ end
54
+
55
+ def display_fields
56
+ self.class.display_fields
57
+ end
58
+
59
+ def private_fields
60
+ self.class.private_fields
61
+ end
62
+
63
+ def private_field?(field_name)
64
+ self.class.private_field?(field_name)
65
+ end
66
+
67
+ def visible_fields
68
+ self.class.visible_fields
69
+ end
70
+
71
+ def visible_field?(field_name)
72
+ self.class.visible_field?(field_name)
73
+ end
74
+
75
+
76
+ def values
77
+ fields.collect { |f| self.send(f) }
78
+ end
79
+
80
+ def initialize(title = "",username = "")
81
+ super()
82
+ self.title = title
83
+ self.username = username
84
+ self.additional_info = ""
85
+ end
86
+
87
+ def needs_container_passphrase?
88
+ false
89
+ end
90
+
91
+ def to_s
92
+ s = StringIO.new
93
+ max_length = self.max_field_length
94
+ fields.each do |f|
95
+ line = "#{f.rjust(max_length + 1)} :"
96
+ value = self.send(f)
97
+ if private_field?(f) then
98
+ # if its private field, then blank out value just to
99
+ value = " ***** private ***** "
100
+ end
101
+ s.puts "#{f.rjust(max_length + 1)} : #{value}"
102
+ end
103
+ return s.string
104
+ end
105
+
106
+ def max_field_length
107
+ fields.collect { |f| f.length }.max
108
+ end
109
+
110
+ end
111
+
112
+ #
113
+ # Host Accounts are those typical login accounts on machines. These
114
+ # contain at a minimum the fields:
115
+ #
116
+ # - title
117
+ # - hostname
118
+ # - username
119
+ # - pasword
120
+ # - additional_info
121
+ #
122
+ # Since HostAccountEntry is a descendant of Keybox::Storage::Record
123
+ # other fields may be added dynamically.
124
+ #
125
+ class HostAccountEntry < Keybox::AccountEntry
126
+
127
+ class << self
128
+ def default_fields
129
+ %w(title hostname username password additional_info)
130
+ end
131
+
132
+ def private_fields
133
+ %w(password)
134
+ end
135
+ end
136
+
137
+ def initialize(title = "",hostname = "",username = "",password = "")
138
+ super(title,username)
139
+ self.hostname = hostname
140
+ self.password = password
141
+ end
142
+
143
+ end
144
+
145
+ #
146
+ # URLAccounts do not have a +password+ field, although it appears
147
+ # to. It is calculated based upon the URL and the master password
148
+ # for the Container. The minimum fields for URLAccountEntry are:
149
+ #
150
+ # - title
151
+ # - url
152
+ # - username
153
+ # - additional_info
154
+ #
155
+ # This is inspired by http://crypto.stanford.edu/PwdHash/ and
156
+ # http://www.xs4all.nl/~jlpoutre/BoT/Javascript/PasswordComposer/
157
+ #
158
+ # This class also needs to be told the container's passphrase to
159
+ # calculate its own password.
160
+ #
161
+ # TODO: Have this class use any other Keybox::Storage::Record
162
+ # for the master password instead of the container.
163
+ #
164
+ class URLAccountEntry < Keybox::AccountEntry
165
+ class << self
166
+ def initial_fields
167
+ %w(title url username additional_info)
168
+ end
169
+
170
+ def private_fields
171
+ %w(password)
172
+ end
173
+ end
174
+
175
+ def initialize(title = "",url = "",username = "")
176
+ super(title,username)
177
+ self.url = url
178
+ end
179
+
180
+ def password_hash_alg
181
+ if not instance_variables.include?("@password_hash_alg") then
182
+ @password_hash_alg = Keybox::PasswordHash.new
183
+ end
184
+ @password_hash_alg
185
+ end
186
+
187
+ def needs_container_passphrase?
188
+ true
189
+ end
190
+
191
+ def container_passphrase=(p)
192
+ password_hash_alg.master_password = p.dup
193
+ end
194
+
195
+ def password
196
+ password_hash_alg.password_for_url(self.url)
197
+ end
198
+
199
+ end
200
+ end
@@ -0,0 +1,5 @@
1
+ module Keybox
2
+ class KeyboxError < ::StandardError ; end
3
+ class ValidationError < KeyboxError; end
4
+ class ApplicationError < KeyboxError; end
5
+ end
@@ -0,0 +1,33 @@
1
+ require 'uri'
2
+ require 'digest/sha1'
3
+ module Keybox
4
+
5
+ # this is an implementation of the password hash algorithm used at
6
+ # http://www.xs4all.nl/~jlpoutre/BoT/Javascript/PasswordComposer/
7
+ #
8
+ # This implementation uses the SHA1 hash instead of the MD5
9
+ #
10
+ # This class uses a master password and with that information
11
+ # generates a unique password for all subsequent strings passed to
12
+ # it.
13
+ #
14
+ class PasswordHash
15
+
16
+ attr_writer :master_password
17
+
18
+ def initialize(master_password = "")
19
+ @master_password = master_password
20
+ @digest_class = ::Digest::SHA1
21
+ end
22
+
23
+ def password_for_url(url)
24
+ uri = URI.parse(url)
25
+ password_for(uri.host)
26
+ end
27
+
28
+ def password_for(str)
29
+ @digest_class.hexdigest("#{@master_password}:#{str}")[0..8]
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,193 @@
1
+ require 'openssl'
2
+ module Keybox
3
+
4
+ ##
5
+ # Use an IO device to retrieve random data. Usually this is
6
+ # somethine like '/dev/random' but may be anything that can read
7
+ # from by IO.read.
8
+ #
9
+ # bytes = RandomDevice.random_bytes(42)
10
+ #
11
+ # or
12
+ #
13
+ # rd = RandomDevice.new
14
+ # bytes = rd.random_bytes(42)
15
+ #
16
+ # If there is a some other hardware device or some service that
17
+ # provides random byte stream, set it as the default device in this
18
+ # class and it will use it instead of the default.
19
+ #
20
+ # my_random_device = RandomDevice("/dev/super-random-device")
21
+ #
22
+ # or
23
+ #
24
+ # RandomDevice.default = "/dev/super-random-device"
25
+ # my_random_device = RandomDevice.new
26
+ # my_random_device.source # => "/dev/super-random-device"
27
+ #
28
+ # All further instances of RandomDevice will use it as the default.
29
+ #
30
+ # RandomDevice can either produce random bytes as a class or as an
31
+ # instance.
32
+ #
33
+ # random_bytes = RandomDevice.random_bytes(42)
34
+ # random_bytes.size # => 42
35
+ #
36
+ class RandomDevice
37
+ @@DEVICES = [ "/dev/urandom", "/dev/random" ]
38
+ @@DEFAULT = nil
39
+
40
+ attr_accessor :source
41
+
42
+ def initialize(device = nil)
43
+ if not device.nil? and File.readable?(device) then
44
+ @source = device
45
+ else
46
+ @source = RandomDevice.default
47
+ end
48
+ end
49
+
50
+ def random_bytes(count = 1)
51
+ File.read(source,count)
52
+ end
53
+
54
+ class << self
55
+ def default
56
+ return @@DEFAULT unless @@DEFAULT.nil?
57
+
58
+ @@DEVICES.each do |device|
59
+ if File.readable?(device) then
60
+ @@DEFAULT = device
61
+ break
62
+ end
63
+ end
64
+ return @@DEFAULT
65
+ end
66
+
67
+ def default=(device)
68
+ if File.readable?(device) then
69
+ @@DEVICES << device
70
+ @@DEFAULT = device
71
+ else
72
+ raise ArgumentError, "device #{device} is not readable and therefore makes a bad random device"
73
+ end
74
+ end
75
+
76
+ def random_bytes(count = 1)
77
+ File.read(RandomDevice.default,count)
78
+ end
79
+ end
80
+ end
81
+
82
+
83
+ #
84
+ # A RandomSource uses one from a set of possible source
85
+ # class/modules. So long as the @@DEFAULT item responds to
86
+ # +random_bytes+ it is fine.
87
+ #
88
+ # RandomSource supplies a +rand+ method in the same vein as
89
+ # Kernel#rand.
90
+ #
91
+ class RandomSource
92
+ @@SOURCE_CLASSES = [ ::Keybox::RandomDevice, ::OpenSSL::Random ]
93
+ @@SOURCE = nil
94
+
95
+ class << self
96
+ def register(klass)
97
+ if klass.respond_to?("random_bytes") then
98
+ @@SOURCE_CLASSES << klass unless @@SOURCE_CLASSES.include?(klass)
99
+ else
100
+ raise ArgumentError, "class #{klass.name} does not have a 'random_bytes' method"
101
+ end
102
+ end
103
+
104
+ def source_classes
105
+ @@SOURCE_CLASSES
106
+ end
107
+
108
+ def source=(klass)
109
+ register(klass)
110
+ @@SOURCE = klass
111
+ end
112
+
113
+ def source
114
+ return @@SOURCE unless @@SOURCE.nil? or not @@SOURCE_CLASSES.include?(@@SOURCE)
115
+ @@SOURCE_CLASSES.each do |klass|
116
+ if klass.random_bytes(2).length == 2 then
117
+ RandomSource.source = klass
118
+ break
119
+ end
120
+ end
121
+ @@SOURCE
122
+ end
123
+
124
+ #
125
+ # Behave like Kernel#rand where if no max is specified return
126
+ # a value >= 0.0 but < 1.0.
127
+ #
128
+ # If a max is specified, return an Integer between 0 and
129
+ # upto but not including max.
130
+ #
131
+ def rand(max = nil)
132
+ bytes = source.random_bytes(8)
133
+ num = bytes.unpack("F").first.abs / Float::MAX
134
+ if max then
135
+ num = bytes.unpack("Q").first % max.floor
136
+ end
137
+ return num
138
+ end
139
+ end
140
+ end
141
+
142
+ #
143
+ # Randomizer will randomly pick a value from anything that
144
+ # behaves like an array or has a +to_a+ method. Behaving like an
145
+ # array means having a +size+ or +length+ method along with an +at+
146
+ # method.
147
+ #
148
+ # The source of randomness is determined at runtime. Any class that
149
+ # can provide a +rand+ method and operates in the same manner
150
+ # as Kernel#rand can be a used as the random source.
151
+ #
152
+ # The item that is being 'picked' from can be any class that has a
153
+ # +size+ method along with an +at+ method.
154
+ #
155
+ # array = ("aaa"..."zzz").to_a
156
+ # r = Randomizer.new
157
+ # r.pick_one_from(array) # => "lho"
158
+ # r.pick_count_from(array,5) # => ["mvt", "tde", "wdu", "ker", "bgc"]
159
+ #
160
+ #
161
+ class Randomizer
162
+
163
+ REQUIRED_METHODS = %w( at size )
164
+
165
+ attr_reader :random_source
166
+
167
+ def initialize(random_source_klass = ::Keybox::RandomSource)
168
+ raise ArgumentError, "Invalid random source class" unless random_source_klass.respond_to?("rand")
169
+ @random_source = random_source_klass
170
+ end
171
+
172
+ def pick_one_from(array)
173
+ pick_count_from(array).first
174
+ end
175
+
176
+ def pick_count_from(array, count = 1)
177
+ raise ArgumentError, "Unable to pick from object of class #{array.class.name}" unless has_correct_duck_type?(array)
178
+ results = []
179
+ range = array.size
180
+ count.times do
181
+ rand_index = random_source.rand(range)
182
+ results << array.at(rand_index)
183
+ end
184
+ results
185
+ end
186
+
187
+ private
188
+
189
+ def has_correct_duck_type?(obj)
190
+ (REQUIRED_METHODS & obj.public_methods).size == REQUIRED_METHODS.size
191
+ end
192
+ end
193
+ end