opensecret 0.0.962 → 0.0.988

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -10
  3. data/bin/opensecret +3 -4
  4. data/bin/ops +5 -0
  5. data/lib/extension/string.rb +114 -0
  6. data/lib/factbase/facts.opensecret.io.ini +9 -21
  7. data/lib/interprete/begin.rb +232 -0
  8. data/lib/interprete/cmd.rb +621 -0
  9. data/lib/{plugins/usecases/unlock.rb → interprete/export.rb} +25 -70
  10. data/lib/interprete/init.rb +205 -0
  11. data/lib/interprete/key.rb +119 -0
  12. data/lib/interprete/open.rb +148 -0
  13. data/lib/{plugins/usecases → interprete}/put.rb +19 -6
  14. data/lib/{plugins/usecases → interprete}/safe.rb +2 -1
  15. data/lib/{plugins/usecases/lock.rb → interprete/seal.rb} +24 -34
  16. data/lib/interprete/set.rb +46 -0
  17. data/lib/interprete/use.rb +43 -0
  18. data/lib/interpreter.rb +165 -0
  19. data/lib/keytools/binary.map.rb +245 -0
  20. data/lib/keytools/digester.rb +245 -0
  21. data/lib/keytools/doc.conversion.to.ones.and.zeroes.ruby +179 -0
  22. data/lib/keytools/doc.rsa.radix.binary-mapping.ruby +190 -0
  23. data/lib/keytools/doc.star.schema.strategy.txt +77 -0
  24. data/lib/keytools/doc.using.pbkdf2.kdf.ruby +95 -0
  25. data/lib/keytools/doc.using.pbkdf2.pkcs.ruby +266 -0
  26. data/lib/keytools/kdf.bcrypt.rb +180 -0
  27. data/lib/keytools/kdf.pbkdf2.rb +164 -0
  28. data/lib/keytools/key.data.rb +227 -0
  29. data/lib/keytools/key.derivation.rb +341 -0
  30. data/lib/keytools/key.module.rb +140 -0
  31. data/lib/keytools/key.rb +481 -0
  32. data/lib/logging/gem.logging.rb +1 -2
  33. data/lib/modules/cryptology.md +43 -0
  34. data/lib/{plugins/ciphers → modules/cryptology}/aes-256.rb +6 -0
  35. data/lib/{crypto → modules/cryptology}/amalgam.rb +6 -0
  36. data/lib/modules/cryptology/blowfish.rb +130 -0
  37. data/lib/modules/cryptology/cipher.rb +207 -0
  38. data/lib/modules/cryptology/collect.rb +118 -0
  39. data/lib/{plugins → modules/cryptology}/crypt.io.rb +5 -0
  40. data/lib/{crypto → modules/cryptology}/engineer.rb +7 -1
  41. data/lib/{crypto → modules/cryptology}/open.bcrypt.rb +0 -0
  42. data/lib/modules/mappers/collateral.rb +282 -0
  43. data/lib/modules/mappers/dictionary.rb +288 -0
  44. data/lib/modules/mappers/envelope.rb +127 -0
  45. data/lib/modules/mappers/settings.rb +170 -0
  46. data/lib/modules/storage/coldstore.rb +186 -0
  47. data/lib/{opensecret/plugins.io/git/git.flow.rb → modules/storage/git.store.rb} +11 -0
  48. data/lib/notepad/scratch.pad.rb +17 -0
  49. data/lib/session/fact.finder.rb +13 -0
  50. data/lib/session/require.gem.rb +5 -0
  51. data/lib/store-commands.txt +180 -0
  52. data/lib/version.rb +1 -1
  53. data/opensecret.gemspec +5 -6
  54. metadata +74 -29
  55. data/lib/crypto/blowfish.rb +0 -85
  56. data/lib/crypto/collect.rb +0 -140
  57. data/lib/crypto/verify.rb +0 -33
  58. data/lib/opensecret.rb +0 -236
  59. data/lib/plugins/cipher.rb +0 -203
  60. data/lib/plugins/ciphers/blowfish.rb +0 -126
  61. data/lib/plugins/coldstore.rb +0 -181
  62. data/lib/plugins/envelope.rb +0 -116
  63. data/lib/plugins/secrets.uc.rb +0 -94
  64. data/lib/plugins/usecase.rb +0 -239
  65. data/lib/plugins/usecases/init.rb +0 -145
  66. data/lib/plugins/usecases/open.rb +0 -108
  67. data/lib/session/attributes.rb +0 -279
  68. data/lib/session/dictionary.rb +0 -191
  69. data/lib/session/file.path.rb +0 -53
  70. data/lib/session/session.rb +0 -80
@@ -3,6 +3,8 @@
3
3
 
4
4
  module OpenSecret
5
5
 
6
+ module ToolBelt
7
+
6
8
  # CryptIO concentrates on injecting and ingesting crypt properties into and
7
9
  # out of a key/value dictionary as well as injecting and ingesting cryptographic
8
10
  # materials into and out of text files.
@@ -217,4 +219,7 @@ module OpenSecret
217
219
  end
218
220
 
219
221
 
222
+ end
223
+
224
+
220
225
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module OpenSecret
4
4
 
5
+ module ToolBelt
6
+
7
+
5
8
  require 'securerandom'
6
9
 
7
10
  # This class will be refactored into an interface implemented by a set
@@ -12,6 +15,7 @@ module OpenSecret
12
15
  # information in the most secure (but simple) manner.
13
16
  class Engineer
14
17
 
18
+
15
19
  # --
16
20
  # -- Get a viable machine password taking into account the human
17
21
  # -- password length and the specified mix_ratio.
@@ -38,7 +42,6 @@ module OpenSecret
38
42
  end
39
43
 
40
44
 
41
-
42
45
  # Amalgamate the parameter passwords using a specific mix ratio. This method
43
46
  # produces cryptographically stronger secrets than algorithms that simply
44
47
  # concatenate two string keys together. If knowledge of one key were gained, this
@@ -90,4 +93,7 @@ module OpenSecret
90
93
  end
91
94
 
92
95
 
96
+ end
97
+
98
+
93
99
  end
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ module OpenSecret
5
+
6
+ module Mapper
7
+
8
+ # <b>Collateral</b> fingers (locates/points to) resource paths and objects that
9
+ # are accessible on the filesystem of either the workstation or its associated
10
+ # (external) drives.
11
+ #
12
+ # Its ubiquitous language distinguishes four (4) base paths for the
13
+ #
14
+ # - <b>frontend store</b> - usually a path to USB connected devices like phones
15
+ # - <b>mid-end store</b> - location is on workstation off the user's home directory
16
+ # - <b>(local) backend store</b> - departure lounge located off the home directory
17
+ # - <b>(remote) backend store</b> - arrival gate on S3, Google Drive or SSH drive
18
+ class Collateral
19
+ include Singleton
20
+
21
+
22
+ # After constructing the <b>{Collateral}</b> object the building
23
+ # blocks that all paths are fabricated from must be injected.
24
+ attr_accessor :domain_name, :frontend_path
25
+
26
+ ENV_OPS_KEY_NAME = "OPS_KEY"
27
+
28
+ # The context name is used in various places (filesystem especially)
29
+ # in order to disambiguate the question of which software creates, reads,
30
+ # and updates the containing collateral.
31
+ CONTEXT_NAME = "opensecret.io"
32
+
33
+ # The name of the frontend directory in which encrypted
34
+ # session envelopes are stored in a tree-like structure.
35
+ SESSION_ENVELOPES_NAME = "session.envelopes"
36
+
37
+ # The name of the frontend directory (library) in which crypt
38
+ # keys are stored in a tree-like structure.
39
+ KEY_STORE_LIBRARY_NAME = "keystore.library"
40
+
41
+ # The name of the backend directory (library) in which crypt
42
+ # material is stored in a tree-like structure.
43
+ CRYPT_STORE_LIBRARY_NAME = "crypt.library"
44
+
45
+ # The name of the frontend directory (library) in which the
46
+ # index files are harboured in a flat structure.
47
+ CONFIG_LIBRARY_NAME = "ops.configuration"
48
+
49
+ # The name of the frontend directory in which the master keys
50
+ # are (or will be) stored.
51
+ MASTER_KEYS_DIR_NAME = "known.master.keys"
52
+
53
+ # The public key filename will end with this constant suffix.
54
+ PUBLIC_KEY_NAME_SUFFIX = "public.key.txt"
55
+
56
+ # The private key filename will end with this constant suffix.
57
+ PRIVATE_KEY_NAME_SUFFIX = "private.key.txt"
58
+
59
+
60
+ # Write the public key text into a file within the master keys
61
+ # folder. The directory path is created if necessary.
62
+ #
63
+ # @param public_key_text [String]
64
+ # the text totality that makes up the domain's public key
65
+ def write_public_key public_key_text
66
+ FileUtils.mkdir_p master_keys_path
67
+ File.write public_key_path, public_key_text
68
+ end
69
+
70
+
71
+ # Read the public key text from a file within the master keys
72
+ # folder. This method will fail if the public key isn't found.
73
+ #
74
+ # @return [String]
75
+ # return the text totality making up the domain's public key
76
+ def read_public_key
77
+ return File.read( public_key_path )
78
+ end
79
+
80
+
81
+ # Write the private key text into a file within the master keys
82
+ # folder. The directory path is created if necessary.
83
+ #
84
+ # @param private_key_text [String]
85
+ # the text totality that makes up the domain's private key
86
+ def write_private_key private_key_text
87
+ FileUtils.mkdir_p master_keys_path
88
+ File.write private_key_path, private_key_text
89
+ end
90
+
91
+
92
+ # Read the private key text from a file within the master keys
93
+ # folder. This method will fail if the private key isn't found.
94
+ #
95
+ # @return [String]
96
+ # return the text totality making up the domain's private key
97
+ def read_private_key
98
+ return File.read( private_key_path )
99
+ end
100
+
101
+
102
+ # Return true if the private key exists within a file under the
103
+ # auspices of the current domain.
104
+ #
105
+ # @param private_key_text [String]
106
+ # the text totality that makes up the domain's private key
107
+ def private_key_exists?
108
+ return File.file? private_key_path
109
+ end
110
+
111
+
112
+ # The path to the initial configuration file below the user's home
113
+ # directory. The directory name the configuration file sits in is
114
+ # a dot prefixed context name derived from the value inside the
115
+ # {Mapper::Collateral::CONTEXT_NAME} constant.
116
+ #
117
+ # ~/.<<context-name>>/<<context-name>>-configuration.ini
118
+ #
119
+ # You can see the filename too is derived from the context with a
120
+ # trailing string ending in <b>.ini</b>.
121
+ #
122
+ # @return [String] full path to the context configuration file
123
+ def config_file
124
+ return File.join config_directory, "#{CONTEXT_NAME}.configuration.ini"
125
+ end
126
+
127
+
128
+ # This method returns the absolute path to the directory that the
129
+ # configuration file sits in. It is basically just the dot prefixed
130
+ # context name (the {Mapper::Collateral::CONTEXT_NAME} constant).
131
+ #
132
+ # ~/.<<context-name>>
133
+ #
134
+ # @return [String] path to directory holding context configuration file
135
+ def config_directory
136
+ return File.join home_directory, ".#{CONTEXT_NAME}"
137
+ end
138
+
139
+
140
+ # The configuration file governing the files and behaviour
141
+ # of the specified domain.
142
+ def domain_config_file
143
+ return File.join config_library_path, "ops.domain.config.ini"
144
+ end
145
+
146
+ # The path within the frontend directory in which the
147
+ # data index and the index configuration files are stored.
148
+ # @return [String]
149
+ # path to the base frontend index library directory
150
+ def config_library_path
151
+ return File.join frontend_base, CONFIG_LIBRARY_NAME
152
+ end
153
+
154
+
155
+
156
+
157
+
158
+ # The path to the session's folder that will be situated
159
+ # within the frontend drive location so that any session
160
+ # materials can be carried away with the external drive.
161
+ def session_folder
162
+ return File.join frontend_base, SESSION_ENVELOPES_NAME
163
+ end
164
+
165
+
166
+ # The path to the session's index file is an amalgam of the
167
+ # session folder {session_folder} and a <b>lowercased</b>
168
+ # session id stamp that should be the first n characters of
169
+ # the session id.
170
+ #
171
+ # @example
172
+ #
173
+ # session id stamp = M4Tywsv3BgNJQSn2MrA3484T
174
+ # session filename = session.m4tywsv3bgnjqsn2mra3484t.txt
175
+ #
176
+ # @param id_stamp [String]
177
+ #
178
+ # the first n characters of the session id that will
179
+ # be lowercased and sandwiched between the words
180
+ # <tt>session.</tt> and <tt>.txt</tt>
181
+ #
182
+ # @return [String]
183
+ # path to the session file that sits inside the frontend
184
+ # drive in folder called <b>session.envelopes</b>
185
+ def session_index_file id_stamp
186
+ return File.join( session_folder, "session.#{id_stamp.downcase}.txt" )
187
+ end
188
+
189
+
190
+
191
+
192
+ # The path within the frontend directory in which the encrypted
193
+ # keystore envelopes live in a tree-like structure.
194
+ # @return [String]
195
+ # path to the base frontend keystore data directory
196
+ def frontend_keystore_path
197
+ return File.join frontend_base, KEY_STORE_LIBRARY_NAME
198
+ end
199
+
200
+
201
+ # The path within the backend directory in which the encrypted
202
+ # crypt envelopes live in a tree-like structure.
203
+ # @return [String]
204
+ # path to the backend crypt store data directory
205
+ def backend_cryptstore_path
206
+ return File.join storage_url, "#{@domain_name}.#{CRYPT_STORE_LIBRARY_NAME}"
207
+ end
208
+
209
+
210
+ # On non-windows systems the home directory is defined
211
+ # perfectly by Ruby's Dir object.
212
+ #
213
+ # On Windows we sometimes get /AppData/Roaming appended
214
+ # onto the actual home directory. In these cases this
215
+ # method removes it.
216
+ #
217
+ # @return [String] the path to the machine user's home directory
218
+ def home_directory
219
+
220
+ return Dir.home unless Gem.win_platform?
221
+
222
+ extraneous_path = "/AppData/Roaming"
223
+ if Dir.home.end_with? extraneous_path then
224
+ return Dir.home.gsub( extraneous_path, "" )
225
+ end
226
+
227
+ return Dir.home
228
+
229
+ end
230
+
231
+
232
+ # Log the configuration file to two places
233
+ #
234
+ # - the console
235
+ # - the log file below the user's home directory.
236
+ #
237
+ # The logging is done at the INFO level.
238
+ def log_config
239
+
240
+ log.info(x) { File.read(config_file).log_lines }
241
+ puts ""
242
+ puts File.read(config_file)
243
+ puts ""
244
+
245
+ end
246
+
247
+
248
+ private
249
+
250
+
251
+ def frontend_base
252
+ return File.join @frontend_path, "ops.#{@domain_name}"
253
+ end
254
+
255
+
256
+ def storage_url
257
+ return File.join config_directory, "#{CONTEXT_NAME}.storage"
258
+ end
259
+
260
+
261
+ def master_keys_path
262
+ return File.join frontend_base, MASTER_KEYS_DIR_NAME
263
+ end
264
+
265
+
266
+ def public_key_path
267
+ return File.join master_keys_path, "#{@domain_name}.#{PUBLIC_KEY_NAME_SUFFIX}"
268
+ end
269
+
270
+
271
+ def private_key_path
272
+ return File.join master_keys_path, "#{@domain_name}.#{PRIVATE_KEY_NAME_SUFFIX}"
273
+ end
274
+
275
+
276
+ end
277
+
278
+
279
+ end
280
+
281
+
282
+ end
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ module OpenKey
5
+
6
+ require 'inifile'
7
+
8
+ # An OpenSession dictionary is a <b>2D (two dimensional) hash</b> data
9
+ # structure backed by an encrypted file.
10
+ #
11
+ # It supports operations to <b>read from</b> and <b>write to</b> a known
12
+ # filepath and given the correct symmetric encryption key it will
13
+ #
14
+ # - decrypt <b>after reading from</b> the file and
15
+ # - encrypt <b>before writing to</b> the file
16
+ #
17
+ # This dictionary extends {Hash} in order to deliver on its core key value
18
+ # storage and retrieve use cases. Extend this dictionary and provide
19
+ # context specific methods through constants to read and write context
20
+ # specific data.
21
+ #
22
+ # == The <em>Current</em> Dictionary Section
23
+ #
24
+ # This Dictionary is <b>two-dimensional</b> so all key-value pairs are stored
25
+ # under the auspices of a section.
26
+ #
27
+ # The Dictionary can track the <b>current section</b> for you and all data
28
+ # exchanges can occur in lieu of a single section if you so wish by using
29
+ # the provided {put} and {get} methods.
30
+ #
31
+ # To employ section management functionality you should pass in a current
32
+ # <b>section id</b> when creating the dictionary.
33
+ #
34
+ # @example
35
+ # To use the dictionary in the raw (unextended) format you create
36
+ # write and read it like this.
37
+ #
38
+ # ----------------------------------------------------------------------
39
+ #
40
+ # my_dictionary = Dictionary.create( "/path/to/backing/file" )
41
+ #
42
+ # my_dictionary["user23"] = {}
43
+ # my_dictionary["user23"]["Name"] = "Joe Bloggs"
44
+ # my_dictionary["user23"]["Email"] = "joebloggs@example.com"
45
+ # my_dictionary["user23"]["Phone"] = "+44 07342 800080"
46
+ #
47
+ # my_dictionary.write( "crypt-key-1234-wxyz" )
48
+ #
49
+ # ----------------------------------------------------------------------
50
+ #
51
+ # my_dictionary = Dictionary.create( "/path/to/backing/file", "crypt-key-1234-wxyz" )
52
+ # puts my_dictionary.has_key? "user23" # => true
53
+ # puts my_dictionary["user23"].length # => 3
54
+ # puts my_dictionary["user23"]["Email"] # => "joebloggs@example.com"
55
+ #
56
+ # ----------------------------------------------------------------------
57
+ class Dictionary < Hash
58
+
59
+ attr_accessor :backing_filepath, :section_id
60
+
61
+
62
+ # Create either a new empty dictionary or unmarshal (deserialize) the
63
+ # dictionary from an encrypted file depending on whether a file exists
64
+ # at the backing_file parameter location.
65
+ #
66
+ # If the backing file indeed exists, the crypt key will be employed to
67
+ # decode and then decrypt the contents before the unmarshal operation.
68
+ #
69
+ # The filepath is stored as an instance variable hence the {write}
70
+ # operation does not need to be told <b>where to?</b>
71
+ #
72
+ # @example
73
+ # # Create Dictionary the first time
74
+ # my_dictionary = Dictionary.create( "/path/to/backing/file" )
75
+ #
76
+ # # Create Dictionary from an Encrypted Backing File
77
+ # my_dictionary = Dictionary.create( "/path/to/backing/file", "crypt-key-1234-wxyz" )
78
+ #
79
+ # @param backing_file [String]
80
+ # the backing file is the filepath to this Dictionary's encrypted
81
+ # backing file when serialized. If no file exists at this path the
82
+ # operation will instantiate and return a new empty {Hash} object.
83
+ #
84
+ # @param crypt_key [String]
85
+ # if the backing file exists then this parameter must contain a
86
+ # robust symmetric decryption key. The symmetric key will be used
87
+ # for decryption after the base64 encoded file is read.
88
+ #
89
+ # Note that the decryption key is never part of the dictionary object.
90
+ # This class method knows it but the new Dictionary has no crypt key
91
+ # instance variable. Another crypt key must then be introduced when
92
+ # serializing (writing) the dictionary back into a file.
93
+ #
94
+ # @return [Dictionary]
95
+ # return a new Dictionary that knows where to go if it needs
96
+ # to read (deserialize) or write (serialize) itself.
97
+ #
98
+ # If no file exists at the path a new empty {Hash} object is
99
+ # returned.
100
+ #
101
+ # If a file exists, then the crypt_key parameter is expected
102
+ # to be the decryption and key and the dictionary will be based
103
+ # on the decrypted contents of the file.
104
+ #
105
+ # @raise [ArgumentError]
106
+ # An {ArgumentError} is raised if either no decryption key is provided
107
+ # or one that is unsuitable (ie was not used within the encryption).
108
+ # Errors can also arise if the block coding and decoding has not been
109
+ # done satisfactorily.
110
+ def self.create backing_file, crypt_key = nil
111
+
112
+ key_missing = File.file?( backing_file ) && crypt_key.nil?
113
+ raise ArgumentError, "No crypt key provided for file #{backing_file}" if key_missing
114
+
115
+ dictionary = Dictionary.new
116
+ dictionary.backing_filepath = backing_file
117
+
118
+ return dictionary unless File.file? backing_file
119
+
120
+ file_contents = File.read( backing_file ).strip
121
+ plaintext_str = file_contents.block_decode_decrypt( crypt_key )
122
+ dictionary.ingest_contents( plaintext_str )
123
+
124
+ return dictionary
125
+
126
+ end
127
+
128
+
129
+ # Create either a new dictionary containing the specified section or unmarshal
130
+ # (deserialize) the dictionary from an encrypted file depending on whether a
131
+ # file exists at the backing_file parameter location and then <b>create</b> the
132
+ # section <b>only if it does not exist</b>.
133
+ #
134
+ # If the backing file indeed exists, the crypt key will be employed to
135
+ # decode and then decrypt the contents before the unmarshal operation.
136
+ #
137
+ # The filepath is stored as an instance variable hence the {write}
138
+ # operation does not need to be told <b>where to?</b>
139
+ #
140
+ # This dictionary will also know which <b>"section"</b> should be used to
141
+ # put, add, update and delete key/value data. You can employ this dictionary
142
+ # such that <b>each instance only creates, updates, removes and/or reads</b>
143
+ # from a single section.
144
+ #
145
+ # @example
146
+ # # Create Dictionary the first time with a section.
147
+ # my_dictionary = Dictionary.create( "/path/to/file", "Europe" )
148
+ #
149
+ # # Create Dictionary from an Encrypted Backing File
150
+ # my_dictionary = Dictionary.create( "/path/to/file", "Europe", "1234-wxyz" )
151
+ #
152
+ # @param backing_file [String]
153
+ # the backing file is the filepath to this Dictionary's encrypted
154
+ # backing file when serialized.
155
+ #
156
+ # @param section_id [String]
157
+ # the created dictionary know which <b>section</b> should be used to
158
+ # put, add, update and delete key/value data. If the backing file
159
+ # does not exist a new section is created in the empty dictionary.
160
+ #
161
+ # If the file exists a new section is created only if it is not
162
+ # already present inside the dictionary.
163
+ #
164
+ # @param crypt_key [String]
165
+ # if the backing file exists then this parameter must contain a
166
+ # robust symmetric decryption key. The symmetric key will be used
167
+ # for decryption after the base64 encoded file is read.
168
+ #
169
+ # Note that the decryption key is never part of the dictionary object.
170
+ # This class method knows it but the new Dictionary has no crypt key
171
+ # instance variable. Another crypt key must then be introduced when
172
+ # serializing (writing) the dictionary back into a file.
173
+ #
174
+ # @return [Dictionary]
175
+ # return a new Dictionary that knows where to go if it needs
176
+ # to read (deserialize) or write (serialize) itself.
177
+ #
178
+ # If no file exists at the path a new empty {Hash} object is
179
+ # returned.
180
+ #
181
+ # If a file exists, then the crypt_key parameter is expected
182
+ # to be the decryption and key and the dictionary will be based
183
+ # on the decrypted contents of the file.
184
+ #
185
+ # @raise [ArgumentError]
186
+ # An {ArgumentError} is raised if either no decryption key is provided
187
+ # or one that is unsuitable (ie was not used within the encryption).
188
+ # Errors can also arise if the block coding and decoding has not been
189
+ # done satisfactorily.
190
+ def self.create_with_section backing_file, section_id, crypt_key = nil
191
+
192
+ dictionary = create( backing_file, crypt_key = nil )
193
+ dictionary.section_id = section_id
194
+ dictionary[section_id] = {} unless dictionary.has_key?( section_id )
195
+
196
+ return dictionary
197
+
198
+ end
199
+
200
+
201
+ # Write the data in this dictionary hash map into a file-system
202
+ # backend mirror whose path was specified in the {Dictionary.create}
203
+ # factory method.
204
+ #
205
+ # Technology for encryption at rest is mandatory when using this
206
+ # Dictionary to write and read files from the filesystem.
207
+ #
208
+ # Calling this {self.write} method when the file at the prescribed path
209
+ # does not exist results in the directory structure being created
210
+ # (if necessary) and then the (possibly encrypted) file being written.
211
+ #
212
+ # @param crypt_key [String]
213
+ # this parameter must contain a robust symmetric crypt key to use for
214
+ # the encryption before writing to the filesystem.
215
+ #
216
+ # Note that the decryption key is never part of the dictionary object.
217
+ # For uncrackable security this key must be changed every time the
218
+ # file is written.
219
+ def write crypt_key
220
+
221
+ ini_file = IniFile.new
222
+ self.each_key do |section_name|
223
+ ini_file[section_name] = self[section_name]
224
+ end
225
+
226
+ crypted_text = ini_file.to_s.encrypt_block_encode( crypt_key )
227
+
228
+ FileUtils.mkdir_p File.dirname(@backing_filepath)
229
+ File.write @backing_filepath, crypted_text
230
+
231
+ end
232
+
233
+
234
+
235
+ def get key_name
236
+ return self[@section_id][key_name]
237
+ end
238
+
239
+
240
+
241
+ def put key_name, key_value
242
+ self[@section_id][key_name] = key_value
243
+ end
244
+
245
+
246
+
247
+
248
+ # Ingest the contents of the INI string and merge it into a
249
+ # this object which is a {Hash}.
250
+ #
251
+ # @param the_contents [String]
252
+ # the INI string that will be ingested and morphed into
253
+ # this dictionary.
254
+ #
255
+ # @raise [ArgumentError]
256
+ # if the content contains any nil section name, key name
257
+ # or key value.
258
+ def ingest_contents the_contents
259
+
260
+ ini_file = IniFile.new( :content => the_contents )
261
+ ini_file.each do | data_group, data_key, data_value |
262
+ ingest_entry data_group, data_key, data_value
263
+ end
264
+
265
+ end
266
+
267
+
268
+ private
269
+
270
+
271
+ def ingest_entry section_name, key_name, value
272
+
273
+ msg = "A NIL object detected during ingestion of file [#{@filepath}]."
274
+ raise ArgumentError.new msg if section_name.nil? || key_name.nil? || value.nil?
275
+
276
+ if self.has_key? section_name then
277
+ self[section_name][key_name] = value
278
+ else
279
+ self.store section_name, { key_name => value }
280
+ end
281
+
282
+ end
283
+
284
+
285
+ end
286
+
287
+
288
+ end