keybox 1.0.0
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/CHANGES +14 -0
- data/COPYING +22 -0
- data/README +132 -0
- data/bin/keybox +18 -0
- data/bin/kpg +19 -0
- data/data/chargrams.txt +8432 -0
- data/lib/keybox.rb +29 -0
- data/lib/keybox/application/base.rb +114 -0
- data/lib/keybox/application/password_generator.rb +131 -0
- data/lib/keybox/application/password_safe.rb +410 -0
- data/lib/keybox/cipher.rb +6 -0
- data/lib/keybox/convert.rb +1 -0
- data/lib/keybox/convert/csv.rb +96 -0
- data/lib/keybox/digest.rb +13 -0
- data/lib/keybox/entry.rb +200 -0
- data/lib/keybox/error.rb +5 -0
- data/lib/keybox/password_hash.rb +33 -0
- data/lib/keybox/randomizer.rb +193 -0
- data/lib/keybox/storage.rb +2 -0
- data/lib/keybox/storage/container.rb +307 -0
- data/lib/keybox/storage/record.rb +103 -0
- data/lib/keybox/string_generator.rb +194 -0
- data/lib/keybox/term_io.rb +163 -0
- data/lib/keybox/uuid.rb +86 -0
- data/spec/base_app_spec.rb +56 -0
- data/spec/convert_csv_spec.rb +46 -0
- data/spec/entry_spec.rb +63 -0
- data/spec/keybox_app_spec.rb +268 -0
- data/spec/kpg_app_spec.rb +132 -0
- data/spec/password_hash_spec.rb +11 -0
- data/spec/randomizer_spec.rb +116 -0
- data/spec/storage_container_spec.rb +99 -0
- data/spec/storage_record_spec.rb +63 -0
- data/spec/string_generator_spec.rb +114 -0
- data/spec/uuid_spec.rb +74 -0
- metadata +83 -0
@@ -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
|