keybox 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ require 'keybox/storage/record'
2
+ require 'keybox/storage/container'
@@ -0,0 +1,307 @@
1
+ require 'keybox/cipher'
2
+ require 'keybox/digest'
3
+ require 'keybox/storage/record'
4
+ require 'keybox/uuid'
5
+ require 'keybox/randomizer'
6
+ require 'keybox/error'
7
+ require 'openssl'
8
+ require 'yaml'
9
+
10
+ module Keybox
11
+ module Storage
12
+ ##
13
+ # The container of the Keybox records. The Container itself is
14
+ # a Keybox::Storage::Record with a few extra methods.
15
+ #
16
+ # A instance of a Container is created with a assprhase and a
17
+ # path to a file. The passphrase can be anything that has a
18
+ # to_s method.
19
+ #
20
+ # container = Keybox::Storage::Container.new("i love ruby", "/tmp/database.yml")
21
+ #
22
+ # This will load from the given file with the given passphrase
23
+ # if the file exists, or it will initalize the container to
24
+ # accept records.
25
+ #
26
+ # The records are held decrypted in memory, so keep that in mind
27
+ # if that is a concern.
28
+ #
29
+ # Add Records
30
+ #
31
+ # record = Keybox::Storage::Record.new
32
+ # record.field1 = "data"
33
+ # record.field1 = "some more data"
34
+ #
35
+ # container << record
36
+ #
37
+ # Delete Records
38
+ #
39
+ # container.delete(record)
40
+ #
41
+ # There is no 'update' record, just delete it and add it.
42
+ #
43
+ # Find a record accepts a string and will look in all the
44
+ # records it contains for anything that matches it. It coerces
45
+ # strings into +Regexp+ so any regex can be used here too.
46
+ #
47
+ # container.find("data")
48
+ #
49
+ # Report if the container or any of its records have been
50
+ # modified:
51
+ #
52
+ # container.modified?
53
+ #
54
+ # Save the container to its default location:
55
+ #
56
+ # container.save
57
+ #
58
+ # Or to some other location
59
+ #
60
+ # container.save("/some/other/path.yml")
61
+ #
62
+ # Direct access to the decrypted records is also available
63
+ # through the +records+ accessor.
64
+ #
65
+ # container.records #=> Array of Keybox::Storage::Record
66
+ #
67
+ class Container < Keybox::Storage::Record
68
+
69
+ attr_reader :records
70
+
71
+ ITERATIONS = 2048
72
+ def initialize(passphrase,path)
73
+ super()
74
+
75
+ @path = path
76
+ @passphrase = passphrase
77
+ @records = []
78
+
79
+ if not load_from_file then
80
+ self.version = Keybox::VERSION
81
+
82
+ self.key_calc_iterations = ITERATIONS
83
+ self.key_digest_salt = Keybox::RandomDevice.random_bytes(32)
84
+ self.key_digest_algorithm = Keybox::Digest::DEFAULT_ALGORITHM
85
+ self.key_digest = calculated_key_digest(passphrase)
86
+
87
+ self.records_init_vector = Keybox::RandomDevice.random_bytes(16)
88
+ self.records_cipher_algorithm = Keybox::Cipher::DEFAULT_ALGORITHM
89
+
90
+ self.records_encrypted_data = ""
91
+ self.records_digest_salt = Keybox::RandomDevice.random_bytes(32)
92
+ self.records_digest_algorithm = Keybox::Digest::DEFAULT_ALGORITHM
93
+ self.records_digest = ""
94
+ end
95
+ end
96
+
97
+ #
98
+ # Change the master password of the container
99
+ #
100
+ def passphrase=(new_passphrase)
101
+ @passphrase = new_passphrase
102
+ self.key_digest_salt = Keybox::RandomDevice.random_bytes(32)
103
+ self.key_digest = calculated_key_digest(new_passphrase)
104
+ self.records_init_vector = Keybox::RandomDevice.random_bytes(16)
105
+ self.records_digest_salt = Keybox::RandomDevice.random_bytes(32)
106
+ end
107
+
108
+ #
109
+ # load from file, if this is successful then replace the
110
+ # existing member fields on this instance with the data from
111
+ # the file
112
+ #
113
+ def load_from_file
114
+ return false unless File.exists?(@path)
115
+ return false unless tmp = YAML.load_file(@path)
116
+ @creation_time = tmp.creation_time
117
+ @modification_time = tmp.modification_time
118
+ @last_access_time = tmp.last_access_time
119
+ @data_members = tmp.data_members
120
+ @uuid = tmp.uuid
121
+ validate_passphrase
122
+ decrypt_records
123
+ validate_decryption
124
+ load_records
125
+ @modified = false
126
+ true
127
+ end
128
+
129
+ #
130
+ # save the current container to a file
131
+ #
132
+ def save(path = @path)
133
+ calculate_records_digest
134
+ encrypt_records
135
+ File.open(path,"w") do |f|
136
+ f.write(self.to_yaml)
137
+ end
138
+
139
+ # mark everything as not modified
140
+ @records.each do |record|
141
+ record.modified = false
142
+ end
143
+ @modified = false
144
+ end
145
+
146
+ #
147
+ # calculate the encryption key from the initial passphrase
148
+ #
149
+ def calculated_key(passphrase = @passphrase)
150
+ key = self.key_digest_salt + passphrase
151
+ self.key_calc_iterations.times do
152
+ key = Keybox::Digest::CLASSES[self.key_digest_algorithm].digest(key)
153
+ end
154
+ return key
155
+ end
156
+
157
+ #
158
+ # calculate the key digest of the encryption key
159
+ #
160
+ def calculated_key_digest(passphrase)
161
+ Keybox::Digest::CLASSES[self.key_digest_algorithm].hexdigest(calculated_key(passphrase))
162
+ end
163
+
164
+ #
165
+ # Add a record to the system
166
+ #
167
+ def <<(obj)
168
+ if obj.respond_to?("needs_container_passphrase?") and obj.needs_container_passphrase? then
169
+ obj.container_passphrase = @passphrase
170
+ end
171
+ @records << obj
172
+ end
173
+
174
+ #
175
+ # The number of Records in the Container
176
+ #
177
+ def length
178
+ @records.size
179
+ end
180
+
181
+ #
182
+ # The number of Records in the Container
183
+ #
184
+ def size
185
+ @records.size
186
+ end
187
+
188
+ #
189
+ # Delete a record from the system, we force a modified flag
190
+ # here since the underlying Record wasn't 'assigned to' we
191
+ # have to force modification notification.
192
+ #
193
+ def delete(obj)
194
+ @records.delete(obj)
195
+ @modified = true
196
+ end
197
+
198
+ #
199
+ # Search only records that have a +url+ field
200
+ #
201
+ def find_by_url(url)
202
+ find(url,%w(url))
203
+ end
204
+
205
+ #
206
+ # Search all the records in the database finding any that
207
+ # match the search string passed in. The Search string is
208
+ # converted to a Regexp before beginning.
209
+ #
210
+ # A list of restricted fields can also be passed in and the
211
+ # regexp will only be matched against those fields
212
+ #
213
+ def find(search_string,restricted_to = nil)
214
+ regex = Regexp.new(search_string)
215
+ matches = []
216
+ @records.each do |record|
217
+ restricted_to = restricted_to || ( record.fields - %w(password) )
218
+ record.data_members.each_pair do |k,v|
219
+ if regex.match(v) and restricted_to.include?(k.to_s) then
220
+ matches << record
221
+ break
222
+ end
223
+ end
224
+ end
225
+ return matches
226
+ end
227
+
228
+ #
229
+ # See if we are modified, or if any of the records are
230
+ # modified
231
+ #
232
+ def modified?
233
+ return true if super
234
+ @records.each do |record|
235
+ return true if record.modified?
236
+ end
237
+ return false
238
+ end
239
+
240
+ private
241
+
242
+ #
243
+ # calculate the key digest of the input pass phrase and
244
+ # compare that to the digest in the data file. If they are
245
+ # the same, then the pass phrase should be the correct one
246
+ # for the data.
247
+ #
248
+ def validate_passphrase
249
+ raise Keybox::ValidationError, "Passphrase digests do not match. The passphrase given does not decrypt the data in this file." unless key_digest == calculated_key_digest(@passphrase)
250
+ end
251
+
252
+ #
253
+ # encrypt the data in the records and store it in
254
+ # records_encrypted_data
255
+ #
256
+ def encrypt_records
257
+ cipher = OpenSSL::Cipher::Cipher.new(self.records_cipher_algorithm)
258
+ cipher.encrypt
259
+ cipher.key = calculated_key
260
+ cipher.iv = self.records_init_vector
261
+ self.records_encrypted_data = cipher.update(@records.to_yaml)
262
+ self.records_encrypted_data << cipher.final
263
+ end
264
+
265
+ #
266
+ # decrypt the data in the record_data and store it in
267
+ # records
268
+ #
269
+ def decrypt_records
270
+ cipher = OpenSSL::Cipher::Cipher.new(self.records_cipher_algorithm)
271
+ cipher.decrypt
272
+ cipher.key = calculated_key
273
+ cipher.iv = self.records_init_vector
274
+ @decrypted_yaml = cipher.update(self.records_encrypted_data)
275
+ @decrypted_yaml << cipher.final
276
+ end
277
+
278
+ #
279
+ # make sure that the decrypted data is validated against its
280
+ # hash
281
+ #
282
+ def validate_decryption
283
+ digest = Keybox::Digest::CLASSES[self.records_digest_algorithm]
284
+ hexdigest = digest.hexdigest(self.records_digest_salt + @decrypted_yaml)
285
+ raise Keybox::ValidationError, "Record digests do not match. The given passphrase does not decrypt the data." unless hexdigest == self.records_digest
286
+ end
287
+
288
+
289
+ def calculate_records_digest
290
+ digest = Keybox::Digest::CLASSES[self.records_digest_algorithm]
291
+ self.records_digest = digest.hexdigest(self.records_digest_salt + @records.to_yaml)
292
+ end
293
+
294
+ def load_records
295
+ @records = YAML.load(@decrypted_yaml)
296
+
297
+ # if a record wants, it can have a reference to the
298
+ # container.
299
+ @records.each do |record|
300
+ if record.respond_to?("needs_container_passphrase?") and record.needs_container_passphrase? then
301
+ record.container_passphrase = @passphrase
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,103 @@
1
+ module Keybox
2
+ module Storage
3
+
4
+ ##
5
+ # Record is very similar to an OStruct but it keeps track of the
6
+ # last access, creation and modification times of the object.
7
+ # Each Record also has a UUID for unique identification.
8
+ #
9
+ # rec = Keybox::Storage::Record.new
10
+ #
11
+ # rec.modified? # => false
12
+ # rec.some_field = "some value" # => "some value"
13
+ # rec.modified? # => true
14
+ # rec.modification_time # => Time
15
+ # rec.data_members # => { :some_field => "some value" }
16
+ # rec.uuid.to_s # => "4b25074d-d3c5-23cd-bf9f-e28d8c8d453d"
17
+ #
18
+ # The +creation_time+, +modification_time+ and +last_access_time+ of
19
+ # each field in the Record is recorded.
20
+ #
21
+ # The UUID is create when the class is instantiated.
22
+ #
23
+ class Record
24
+
25
+ attr_reader :creation_time
26
+ attr_reader :modification_time
27
+ attr_reader :last_access_time
28
+ attr_reader :uuid
29
+ attr_reader :data_members
30
+
31
+ PROTECTED_METHODS = [ :creation_time=, :modification_time=, :last_access_time=,
32
+ :uuid=, :data_members=, :modified, ]
33
+ def initialize
34
+ @creation_time = Time.now
35
+ @modification_time = @creation_time.dup
36
+ @last_access_time = @creation_time.dup
37
+ @uuid = Keybox::UUID.new
38
+ @data_members = Hash.new
39
+ @modified = false
40
+ end
41
+
42
+ def modified?
43
+ # since this class can be loaded from a YAML file and
44
+ # modified is not stored in the serialized format, if
45
+ # @modified is not initialized, initialize it.
46
+ if not instance_variables.include?("@modified") then
47
+ @modified = false
48
+ end
49
+ @modified
50
+ end
51
+
52
+ # this is here so that after this class has been serialized
53
+ # to a file the container can mark it as clean. The class
54
+ # should take care of marking itself dirty.
55
+ def modified=(value)
56
+ @modified = value
57
+ end
58
+
59
+ def method_missing(method_id, *args)
60
+ method_name = method_id.id2name
61
+ member_sym = method_name.gsub(/=/,'').to_sym
62
+
63
+ # guard against assigning to the time data members and
64
+ # the data_members element
65
+ if PROTECTED_METHODS.include?(method_id) then
66
+ raise NoMethodError, "invalid method #{method_name} for #{self.class.name}", caller(1)
67
+ end
68
+
69
+ # if the method ends with '=' and has a single argument,
70
+ # then convert the name to the appropriate data member
71
+ # and store the argument in the hash.
72
+ if method_name[-1].chr == "=" then
73
+ raise ArgumentError, "'#{method_name}' requires one and only one argument", caller(1) unless args.size == 1
74
+ @data_members[member_sym] = args[0]
75
+ @modified = true
76
+ @modification_time = Time.now
77
+ @last_access_time = @modification_time.dup
78
+ elsif args.size == 0 then
79
+ @last_access_time = Time.now
80
+ @data_members[member_sym]
81
+ else
82
+ raise NoMethodError, "undefined method #{method_name} for #{self.class.name}", caller(1)
83
+ end
84
+ end
85
+
86
+ def to_yaml_properties
87
+ %w{ @creation_time @modification_time @last_access_time @data_members @uuid }
88
+ end
89
+
90
+ def ==(other)
91
+ self.eql?(other)
92
+ end
93
+
94
+ def eql?(other)
95
+ if other.kind_of?(Keybox::Storage::Record) then
96
+ self.uuid == other.uuid
97
+ else
98
+ self.uuid == other
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,194 @@
1
+ require 'keybox/randomizer'
2
+
3
+ module Keybox
4
+
5
+ # base class for string generation. A string generator can be
6
+ # called serially to generate a new string in chunks, or all at once
7
+ # to generate one of a certain size.
8
+ class StringGenerator
9
+ attr_accessor :chunks
10
+ attr_accessor :min_length
11
+ attr_accessor :max_length
12
+ attr_accessor :autoclear
13
+
14
+ def initialize
15
+ @chunks = Array.new
16
+ @min_length = 8
17
+ @max_length = 10
18
+ @randomizer = Keybox::Randomizer.new
19
+ @autoclear = true
20
+ end
21
+
22
+ def generate
23
+ clear if autoclear
24
+ generate_chunk until valid?
25
+ self.to_s
26
+ end
27
+
28
+ def clear
29
+ @chunks.clear
30
+ end
31
+
32
+ def to_s
33
+ @chunks.join('')[0...@max_length]
34
+ end
35
+
36
+ def valid?
37
+ raise Keybox::ValidationError, "max_length (#{max_length}) must be greater than or equal to min_length (#{min_length})", caller if max_length < min_length
38
+ @chunks.join('').size >= @min_length
39
+ end
40
+
41
+ def generate_chunk
42
+ raise Keybox::KeyboxError, "generate_chunk Not Implemented"
43
+ end
44
+
45
+ end
46
+
47
+ # CharGramGenerator emits a sequence that is a CharGram of length N.
48
+ # The ngrams can be the default ones that ship with keybox or an
49
+ # array passed in.
50
+ class CharGramGenerator < StringGenerator
51
+ attr_reader :pool
52
+
53
+
54
+ def initialize(chargram_list = nil)
55
+ super()
56
+ wordlist = chargram_list || load_default_chargram_list
57
+ @pool = Hash.new
58
+ wordlist.each do |word|
59
+ letters = word.split('')
60
+ letter_set = @pool[letters.first] ||= Array.new
61
+ letter_set << word
62
+ end
63
+ end
64
+
65
+ def size
66
+ @pool.inject(0) { |sum,h| sum + h[1].size }
67
+ end
68
+
69
+ # return a random chargram from the pool indidcated by the last
70
+ # character of the last emitted item
71
+ def generate_chunk
72
+ pool_key = ""
73
+ if @chunks.size > 0 then
74
+ pool_key = @chunks.pop
75
+ else
76
+ pool_key = @randomizer.pick_one_from(@pool.keys)
77
+ end
78
+ new_chunk = @randomizer.pick_one_from(@pool[pool_key])
79
+ @chunks.concat(new_chunk.split(//))
80
+ new_chunk
81
+ end
82
+
83
+ private
84
+
85
+ def load_default_chargram_list
86
+ list = []
87
+ File.open(File.join(Keybox::APP_DATA_DIR,"chargrams.txt")) do |f|
88
+ f.each_line do |line|
89
+ next if line =~ /^#/
90
+ list << line.rstrip
91
+ end
92
+ end
93
+ list
94
+ end
95
+ end
96
+
97
+ module SymbolSet
98
+ LOWER_ASCII = ("a".."z").to_a
99
+ UPPER_ASCII = ("A".."Z").to_a
100
+ NUMERAL_ASCII = ("0".."9").to_a
101
+ SPECIAL_ASCII = ("!".."/").to_a + (":".."@").to_a + %w( [ ] ^ _ { } | ~ )
102
+ ALL = LOWER_ASCII + UPPER_ASCII + NUMERAL_ASCII + SPECIAL_ASCII
103
+
104
+ MAPPING = {
105
+ "lower" => LOWER_ASCII,
106
+ "upper" => UPPER_ASCII,
107
+ "numerical" => NUMERAL_ASCII,
108
+ "special" => SPECIAL_ASCII,
109
+ "all" => ALL
110
+ }
111
+ end
112
+
113
+ #
114
+ # SymbolSetGenerator uses symbol sets and emits a single character
115
+ # from the aggregated symobl sets that the instance is wrapping.
116
+ #
117
+ # That is When a SymbolSetGenerator is instantiated, it can take a
118
+ # list of symbol sets (a-z, A-Z, 0-9) etc as parameters. Those sets
119
+ # are merged and when 'emit' is called a string of length 1 is
120
+ # returned from the aggregated symbols
121
+ #
122
+ class SymbolSetGenerator < StringGenerator
123
+ include SymbolSet
124
+
125
+ attr_accessor :required_sets
126
+
127
+ def initialize(set = ALL)
128
+ super()
129
+ @symbols = set.flatten.uniq
130
+ @required_sets = []
131
+ end
132
+
133
+ # every time we access the symbols set we need to make sure that
134
+ # the required symbols are a part of it, and if they aren't then
135
+ # make sure they are.
136
+ def symbols
137
+ if @required_sets.size > 0 then
138
+ if not @symbols.include?(@required_sets.first[0]) then
139
+ @symbols << @required_sets
140
+ @symbols.flatten!
141
+ @symbols.uniq!
142
+ end
143
+ end
144
+ @symbols
145
+ end
146
+
147
+ def required_generated?
148
+ result = true
149
+ @required_sets.each do |set|
150
+ set_found = false
151
+ @chunks.each do |chunk|
152
+ if set.include?(chunk) then
153
+ set_found = true
154
+ break
155
+ end
156
+ end
157
+ result = (result and set_found)
158
+ end
159
+ return result
160
+ end
161
+
162
+ # we force generation of the required sets at the beginning the
163
+ # first time we are called.
164
+ def generate_chunk
165
+ chunk = ""
166
+ if required_generated? then
167
+ @chunks << @randomizer.pick_one_from(symbols)
168
+ chunk = @chunks.last
169
+ else
170
+ req = generate_required
171
+ @chunks.concat(req)
172
+ chunk = req.join('')
173
+ end
174
+ return chunk
175
+ end
176
+
177
+ # valid for the symbol set generator means the parent classes
178
+ # validity and that we have in the
179
+ def valid?
180
+ super and required_generated?
181
+ end
182
+
183
+ private
184
+
185
+ # generate symbols from the required sets
186
+ def generate_required
187
+ req = []
188
+ required_sets.each do |set|
189
+ req << @randomizer.pick_one_from(set)
190
+ end
191
+ return req
192
+ end
193
+ end
194
+ end