Class: SafeDb::Dictionary

Inherits:
Hash
  • Object
show all
Defined in:
lib/modules/mappers/dictionary.rb

Overview

An OpenSession dictionary is a 2D (two dimensional) hash data structure backed by an encrypted file.

It supports operations to read from and write to a known filepath and given the correct symmetric encryption key it will

  • decrypt after reading from the file and

  • encrypt before writing to the file

This dictionary extends Hash in order to deliver on its core key value storage and retrieve use cases. Extend this dictionary and provide context specific methods through constants to read and write context specific data.

The Current Dictionary Section

This Dictionary is two-dimensional so all key-value pairs are stored under the auspices of a section.

The Dictionary can track the current section for you and all data exchanges can occur in lieu of a single section if you so wish by using the provided #put and #get methods.

To employ section management functionality you should pass in a current section id when creating the dictionary.

Examples:

To use the dictionary in the raw (unextended) format you create
write and read it like this.

----------------------------------------------------------------------

my_dictionary = Dictionary.create( "/path/to/backing/file" )

my_dictionary["user23"] = {}
my_dictionary["user23"]["Name"] = "Joe Bloggs"
my_dictionary["user23"]["Email"] = "[email protected]"
my_dictionary["user23"]["Phone"] = "+44 07342 800080"

my_dictionary.write( "crypt-key-1234-wxyz" )

----------------------------------------------------------------------

my_dictionary = Dictionary.create( "/path/to/backing/file", "crypt-key-1234-wxyz" )
puts my_dictionary.has_key? "user23"   # => true
puts my_dictionary["user23"].length    # => 3
puts my_dictionary["user23"]["Email"]  # => "[email protected]"

----------------------------------------------------------------------

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Hash

#log_contents

Instance Attribute Details

#backing_filepathObject

Returns the value of attribute backing_filepath.



59
60
61
# File 'lib/modules/mappers/dictionary.rb', line 59

def backing_filepath
  @backing_filepath
end

#section_idObject

Returns the value of attribute section_id.



59
60
61
# File 'lib/modules/mappers/dictionary.rb', line 59

def section_id
  @section_id
end

Class Method Details

.create(backing_file, crypt_key = nil) ⇒ Dictionary

Create either a new empty dictionary or unmarshal (deserialize) the dictionary from an encrypted file depending on whether a file exists at the backing_file parameter location.

If the backing file indeed exists, the crypt key will be employed to decode and then decrypt the contents before the unmarshal operation.

The filepath is stored as an instance variable hence the #write operation does not need to be told where to?

Examples:

# Create Dictionary the first time
my_dictionary = Dictionary.create( "/path/to/backing/file" )

# Create Dictionary from an Encrypted Backing File
my_dictionary = Dictionary.create( "/path/to/backing/file", "crypt-key-1234-wxyz" )

Parameters:

  • backing_file (String)

    the backing file is the filepath to this Dictionary’s encrypted backing file when serialized. If no file exists at this path the operation will instantiate and return a new empty Hash object.

  • crypt_key (String) (defaults to: nil)

    if the backing file exists then this parameter must contain a robust symmetric decryption key. The symmetric key will be used for decryption after the base64 encoded file is read.

    Note that the decryption key is never part of the dictionary object. This class method knows it but the new Dictionary has no crypt key instance variable. Another crypt key must then be introduced when serializing (writing) the dictionary back into a file.

Returns:

  • (Dictionary)

    return a new Dictionary that knows where to go if it needs to read (deserialize) or write (serialize) itself.

    If no file exists at the path a new empty Hash object is returned.

    If a file exists, then the crypt_key parameter is expected to be the decryption and key and the dictionary will be based on the decrypted contents of the file.

Raises:

  • (ArgumentError)

    An ArgumentError is raised if either no decryption key is provided or one that is unsuitable (ie was not used within the encryption). Errors can also arise if the block coding and decoding has not been done satisfactorily.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/modules/mappers/dictionary.rb', line 110

def self.create backing_file, crypt_key = nil

  key_missing = File.file?( backing_file ) && crypt_key.nil?
  raise ArgumentError, "No crypt key provided for file #{backing_file}" if key_missing

  dictionary = Dictionary.new
  dictionary.backing_filepath = backing_file

  return dictionary unless File.file? backing_file

  file_contents = File.read( backing_file ).strip
  plaintext_str = file_contents.block_decode_decrypt( crypt_key )
  dictionary.ingest_contents( plaintext_str )

  return dictionary
  
end

.create_with_section(backing_file, section_id, crypt_key = nil) ⇒ Dictionary

Create either a new dictionary containing the specified section or unmarshal (deserialize) the dictionary from an encrypted file depending on whether a file exists at the backing_file parameter location and then create the section only if it does not exist.

If the backing file indeed exists, the crypt key will be employed to decode and then decrypt the contents before the unmarshal operation.

The filepath is stored as an instance variable hence the #write operation does not need to be told where to?

This dictionary will also know which “section” should be used to put, add, update and delete key/value data. You can employ this dictionary such that each instance only creates, updates, removes and/or reads from a single section.

Examples:

# Create Dictionary the first time with a section.
my_dictionary = Dictionary.create( "/path/to/file", "Europe" )

# Create Dictionary from an Encrypted Backing File
my_dictionary = Dictionary.create( "/path/to/file", "Europe", "1234-wxyz" )

Parameters:

  • backing_file (String)

    the backing file is the filepath to this Dictionary’s encrypted backing file when serialized.

  • section_id (String)

    the created dictionary know which section should be used to put, add, update and delete key/value data. If the backing file does not exist a new section is created in the empty dictionary.

    If the file exists a new section is created only if it is not already present inside the dictionary.

  • crypt_key (String) (defaults to: nil)

    if the backing file exists then this parameter must contain a robust symmetric decryption key. The symmetric key will be used for decryption after the base64 encoded file is read.

    Note that the decryption key is never part of the dictionary object. This class method knows it but the new Dictionary has no crypt key instance variable. Another crypt key must then be introduced when serializing (writing) the dictionary back into a file.

Returns:

  • (Dictionary)

    return a new Dictionary that knows where to go if it needs to read (deserialize) or write (serialize) itself.

    If no file exists at the path a new empty Hash object is returned.

    If a file exists, then the crypt_key parameter is expected to be the decryption and key and the dictionary will be based on the decrypted contents of the file.

Raises:

  • (ArgumentError)

    An ArgumentError is raised if either no decryption key is provided or one that is unsuitable (ie was not used within the encryption). Errors can also arise if the block coding and decoding has not been done satisfactorily.



190
191
192
193
194
195
196
197
198
# File 'lib/modules/mappers/dictionary.rb', line 190

def self.create_with_section backing_file, section_id, crypt_key = nil

  dictionary = create( backing_file, crypt_key = nil )
  dictionary.section_id = section_id
  dictionary[section_id] = {} unless dictionary.has_key?( section_id )

  return dictionary
  
end

Instance Method Details

#get(key_name) ⇒ Object



235
236
237
# File 'lib/modules/mappers/dictionary.rb', line 235

def get key_name
  return self[@section_id][key_name]
end

#ingest_contents(the_contents) ⇒ Object

Ingest the contents of the INI string and merge it into a this object which is a Hash.

Parameters:

  • the_contents (String)

    the INI string that will be ingested and morphed into this dictionary.

Raises:

  • (ArgumentError)

    if the content contains any nil section name, key name or key value.



258
259
260
261
262
263
264
265
# File 'lib/modules/mappers/dictionary.rb', line 258

def ingest_contents the_contents
  
  ini_file = IniFile.new( :content => the_contents )
  ini_file.each do | data_group, data_key, data_value |
    ingest_entry data_group, data_key, data_value
  end

end

#put(key_name, key_value) ⇒ Object



241
242
243
# File 'lib/modules/mappers/dictionary.rb', line 241

def put key_name, key_value
  self[@section_id][key_name] = key_value
end

#write(crypt_key) ⇒ Object

Write the data in this dictionary hash map into a file-system backend mirror whose path was specified in the create factory method.

Technology for encryption at rest is mandatory when using this Dictionary to write and read files from the filesystem.

Calling this SafeDb::Dictionary.selfself.write method when the file at the prescribed path does not exist results in the directory structure being created (if necessary) and then the (possibly encrypted) file being written.

Parameters:

  • crypt_key (String)

    this parameter must contain a robust symmetric crypt key to use for the encryption before writing to the filesystem.

    Note that the decryption key is never part of the dictionary object. For uncrackable security this key must be changed every time the file is written.



219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/modules/mappers/dictionary.rb', line 219

def write crypt_key

  ini_file = IniFile.new
  self.each_key do |section_name|
    ini_file[section_name] = self[section_name]
  end

  crypted_text = ini_file.to_s.encrypt_block_encode( crypt_key )

  FileUtils.mkdir_p File.dirname(@backing_filepath)
  File.write @backing_filepath, crypted_text

end