class OpenKey::Dictionary

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

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.

@example

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"] = "joebloggs@example.com"
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"]  # => "joebloggs@example.com"

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

Attributes

backing_filepath[RW]
section_id[RW]

Public Class Methods

create(backing_file, crypt_key = nil) click to toggle source

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?

@example

# 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" )

@param 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.

@param crypt_key [String]

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.

@return [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.

@raise [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.
# 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) click to toggle source

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.

@example

# 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" )

@param backing_file [String]

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

@param section_id [String]

the created dictionary know which <b>section</b> 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.

@param crypt_key [String]

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.

@return [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.

@raise [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.
# 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

Public Instance Methods

get(key_name) click to toggle source
# File lib/modules/mappers/dictionary.rb, line 235
def get key_name
  return self[@section_id][key_name]
end
ingest_contents(the_contents) click to toggle source

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

@param the_contents [String]

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

@raise [ArgumentError]

if the content contains any nil section name, key name
or key value.
# 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) click to toggle source
# 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) click to toggle source

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

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

Calling this {self.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.

@param 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.
# 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

Private Instance Methods

ingest_entry(section_name, key_name, value) click to toggle source
# File lib/modules/mappers/dictionary.rb, line 271
def ingest_entry section_name, key_name, value

  msg = "A NIL object detected during ingestion of file [#{@filepath}]."
  raise ArgumentError.new msg if section_name.nil? || key_name.nil? || value.nil?

  if self.has_key? section_name then
    self[section_name][key_name] = value
  else
    self.store section_name, { key_name => value }
  end

end