Class: SafeDb::Book
- Inherits:
-
Object
- Object
- SafeDb::Book
- Defined in:
- lib/model/book.rb
Overview
The book index is pretty much like the index at the front of a book!
With a real book the index tells you the page number of a chapter. Our Book knows about and behaves with concepts like
-
the list of chapters in the book including the chapter names
-
attributes like book name, creation and last accessed dates
-
book scoped configuration directives and their values
-
chapter keys including file content ids and encryption keys
Parental use cases in the background will use this index to encrypt, decrypt create, read, update and delete chapter encased credentials.
Instance Method Summary collapse
-
#book_id ⇒ String
Returns the id number of the safe book.
-
#book_name ⇒ String
Returns the name of the safe book.
-
#branch_chapter_keys ⇒ DataStore
Returns a map of chapter keys that exist within the current branch (line) of this book.
-
#branch_id ⇒ String
Returns the id number of the current safe branch.
-
#can_commit? ⇒ Boolean
Return true if the commit identifiers for the master and the branch match meaning that we can commit (commit).
-
#chapter_count ⇒ Numeric
Get the number of chapters nestled within this book.
-
#get_branch_verse_count ⇒ Number
Get the number of verses in the branch’s data structure.
-
#get_master_verse_count ⇒ Number
Get the number of verses in the master’s data structure.
-
#get_open_chapter_data ⇒ DataStore
Returns the data structure corresponding to the book’s open chapter.
-
#get_open_chapter_keys ⇒ DataStore
If chapter keys exist for the open chapter this method returns them.
-
#get_open_chapter_name ⇒ String
Returns the name of the chapter that this book has been opened at.
-
#get_open_verse_data ⇒ DataStore
Returns the data structure corresponding to the book’s open verse within the book’s open chapter.
-
#get_open_verse_name ⇒ String
Returns the name of the verse that this book has been opened at.
-
#has_open_chapter_data? ⇒ Boolean
Returns true if this book index has a chapter name specified to be the open chapter.
-
#has_open_chapter_name? ⇒ Boolean
Returns true if this book index has a chapter name specified to be the open chapter.
-
#has_open_verse_data? ⇒ Boolean
Returns true if this book index has a verse name specified to be the open verse.
-
#has_open_verse_name? ⇒ Boolean
Returns true if this book index has a verse name specified to be the open verse.
-
#import_chapter(chapter_name, chapter_data) ⇒ Object
Import and persist the parameter data structure into this book with the parameter chapter name using a deep merge that recursively seeks to preserve all non-duplicate records in both the source and destination structures.
-
#init_time ⇒ String
Returns the human readable date/time denoting when the book was first initialized.
-
#init_version ⇒ String
Returns the safedb application software version at the time that the safe book was initialized.
-
#initialize ⇒ Book
constructor
Initialize the book index data structure from the branch state file and the current branch identifier.
-
#is_open?(chapter_name, verse_name) ⇒ Boolean
Are both the chapter and verse names in the parameters open? An exception is thrown if any of the parameters are nil.
-
#is_open_chapter?(this_chapter_name) ⇒ Boolean
Is the chapter name in the parameter the book’s open chapter? An exception is thrown if the parameter chapter name is nil.
-
#is_open_verse?(this_verse_name) ⇒ Boolean
Is the verse name in the parameter the book’s open verse? An exception is thrown if the parameter verse name is nil.
-
#is_opened? ⇒ Boolean
Has this book been opened at a chapter and verse location.
-
#print_book_mark ⇒ Object
Print a notie stating the book followed by and open chapter and verse names only if the book is currently opened at a specific chapter and verse.
-
#read ⇒ String
Construct a Book object that extends the DataStore data structure which in turns extens the Ruby hash object.
-
#set_master_chapter_keys ⇒ Object
Initializes the master book index chapter keys by using the @crypt_key along with the random iv and content id (read from the master indices) to decrypt the ciphertext in a master crypt file (found using the book id and content id).
-
#set_open_chapter_data ⇒ Object
Persist the instantiated chapter data structure including all its verses.
-
#set_open_chapter_name(chapter_name) ⇒ Object
Sets the name of the chapter that this book is to be opened at.
-
#set_open_verse_name(verse_name) ⇒ Object
Sets the name of the verse that this book is to be opened at.
-
#to_branch_data ⇒ Object
Get the hash data structure representing the branch’s state.
-
#to_master_data ⇒ Object
Get the hash data structure representing the master’s state.
-
#unopened_chapter_verse? ⇒ Boolean
Return true if this book has NOT been opened at a chapter and verse location.
-
#write ⇒ Object
This write content behaviour takes the parameter content, encyrpts and encodes it using the index key, which is itself derived from the shell key unlocking the intra branch ciphertext.
-
#write_open_chapter ⇒ Object
Write data for the open chapter to the configured safe store.
Constructor Details
#initialize ⇒ Book
Initialize the book index data structure from the branch state file and the current branch identifier.
We assume that something else created the very first book index so we never check whether it exists, instead we assume that one does exist.
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/model/book.rb', line 26 def initialize @branch_id = Identifier.derive_branch_id( Branch.to_token() ) @branch_keys = DataMap.new( FileTree.branch_indices_filepath( @branch_id ) ) @book_id = @branch_keys.read( Indices::BRANCH_DATA, Indices::CURRENT_BRANCH_BOOK_ID ) @branch_keys.use( @book_id ) @content_id = @branch_keys.get( Indices::CONTENT_IDENTIFIER ) @master_keys = DataMap.new( Indices::MASTER_INDICES_FILEPATH ) @master_keys.use( @book_id ) intra_key_ciphertext = @branch_keys.get( Indices::CRYPT_CIPHER_TEXT ) intra_key = KeyDerivation.regenerate_shell_key( Branch.to_token() ) @crypt_key = intra_key.do_decrypt_key( intra_key_ciphertext ) read() end |
Instance Method Details
#book_id ⇒ String
Returns the id number of the safe book.
449 450 451 |
# File 'lib/model/book.rb', line 449 def book_id() return @book_id end |
#book_name ⇒ String
Returns the name of the safe book.
442 443 444 |
# File 'lib/model/book.rb', line 442 def book_name() return @book[ Indices::SAFE_BOOK_NAME ] end |
#branch_chapter_keys ⇒ DataStore
Returns a map of chapter keys that exist within the current branch (line) of this book. An empty map will be returned if no data has been added as yet to the book.
493 494 495 |
# File 'lib/model/book.rb', line 493 def branch_chapter_keys() return @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ] end |
#branch_id ⇒ String
Returns the id number of the current safe branch
456 457 458 |
# File 'lib/model/book.rb', line 456 def branch_id() return @branch_id end |
#can_commit? ⇒ Boolean
Return true if the commit identifiers for the master and the branch match meaning that we can commit (commit).
382 383 384 |
# File 'lib/model/book.rb', line 382 def can_commit?() return @branch_keys.get( Indices::COMMIT_IDENTIFIER ).eql?( @master_keys.get( Indices::COMMIT_IDENTIFIER ) ) end |
#chapter_count ⇒ Numeric
Get the number of chapters nestled within this book.
364 365 366 |
# File 'lib/model/book.rb', line 364 def chapter_count() return branch_chapter_keys().length() end |
#get_branch_verse_count ⇒ Number
Get the number of verses in the branch’s data structure.
95 96 97 |
# File 'lib/model/book.rb', line 95 def get_branch_verse_count() return @branch_verse_count end |
#get_master_verse_count ⇒ Number
Get the number of verses in the master’s data structure.
67 68 69 |
# File 'lib/model/book.rb', line 67 def get_master_verse_count() return @master_verse_count end |
#get_open_chapter_data ⇒ DataStore
Returns the data structure corresponding to the book’s open chapter. If has_open_chapter_name?() returns false this method will throw an exception.
269 270 271 272 273 274 275 |
# File 'lib/model/book.rb', line 269 def get_open_chapter_data() raise RuntimeError, "Cannot read data as no chapter is open." unless has_open_chapter_name?() return @open_chapter_data unless @open_chapter_data.nil?() @open_chapter_data = DataStore.new unless has_open_chapter_data?() @open_chapter_data = Content.unlock_branch_chapter( get_open_chapter_keys() ) if has_open_chapter_data?() return @open_chapter_data end |
#get_open_chapter_keys ⇒ DataStore
If chapter keys exist for the open chapter this method returns them. If not, it creates an in-place empty map and returns that.
If no chapter in this book has been opened, signalled by has_open_chapter_name?() an exception is thrown.
259 260 261 262 263 |
# File 'lib/model/book.rb', line 259 def get_open_chapter_keys() raise RuntimeError, "Cannot get chapter keys as no chapter is open." unless has_open_chapter_name?() @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ][ get_open_chapter_name() ] = DataStore.new() unless has_open_chapter_data?() return @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ][ get_open_chapter_name() ] end |
#get_open_chapter_name ⇒ String
Returns the name of the chapter that this book has been opened at. If has_open_chapter_name?() returns false this method will throw an exception.
212 213 214 215 |
# File 'lib/model/book.rb', line 212 def get_open_chapter_name() raise RuntimeError, "No chapter has been opened." unless has_open_chapter_name?() return @book[ Indices::OPENED_CHAPTER_NAME ] end |
#get_open_verse_data ⇒ DataStore
Returns the data structure corresponding to the book’s open verse within the book’s open chapter. Both the open chapter and verse names have to be set otherwise an exception will be thrown.
306 307 308 309 |
# File 'lib/model/book.rb', line 306 def get_open_verse_data() get_open_chapter_data()[ get_open_verse_name() ] = DataStore.new() unless has_open_verse_data?() return get_open_chapter_data()[ get_open_verse_name() ] end |
#get_open_verse_name ⇒ String
Returns the name of the verse that this book has been opened at. If has_open_verse_name?() returns false this method will throw an exception.
221 222 223 224 |
# File 'lib/model/book.rb', line 221 def get_open_verse_name() raise RuntimeError, "No verse has been opened." unless has_open_verse_name?() return @book[ Indices::OPENED_VERSE_NAME ] end |
#has_open_chapter_data? ⇒ Boolean
Returns true if this book index has a chapter name specified to be the open chapter. True is returned even if the open chapter data structure is empty.
247 248 249 250 |
# File 'lib/model/book.rb', line 247 def has_open_chapter_data?() raise RuntimeError, "Unopened chapter prevents data availability check." unless has_open_chapter_name?() return @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ].has_key?( get_open_chapter_name() ) end |
#has_open_chapter_name? ⇒ Boolean
Returns true if this book index has a chapter name specified to be the open chapter. True is returned even if the open chapter data structure is empty.
195 196 197 |
# File 'lib/model/book.rb', line 195 def has_open_chapter_name?() return @book.has_key?( Indices::OPENED_CHAPTER_NAME ) end |
#has_open_verse_data? ⇒ Boolean
Returns true if this book index has a verse name specified to be the open verse. True is returned even if the open verse data structure is empty.
293 294 295 296 297 298 299 |
# File 'lib/model/book.rb', line 293 def has_open_verse_data?() raise RuntimeError, "Cannot look for verse as no open chapter." unless has_open_chapter_name?() raise RuntimeError, "Cannot look for verse as no open verse." unless has_open_verse_name?() chapter_data = get_open_chapter_data() return false if chapter_data.empty?() return chapter_data.has_key?( get_open_verse_name() ) end |
#has_open_verse_name? ⇒ Boolean
Returns true if this book index has a verse name specified to be the open verse. True is returned even if the open verse data structure is empty.
204 205 206 |
# File 'lib/model/book.rb', line 204 def has_open_verse_name?() return @book.has_key?( Indices::OPENED_VERSE_NAME ) end |
#import_chapter(chapter_name, chapter_data) ⇒ Object
Import and persist the parameter data structure into this book with the parameter chapter name using a deep merge that recursively seeks to preserve all non-duplicate records in both the source and destination structures.
Chapter Alreay Exists
What if a chapter of the given name already exists?
In this case we merge the new (incoming) chapter data into the old chapter data. Existing verses not declared in the incoming chapter data continue to live on.
Duplicates occur when the same keys posses different values at any level including verse and line (even sub-line levels for assimilated files).The default is to favour incoming values when duplicates are encountered.
Parameter Validation
Neither of the parameters should be nil or empty, nor should the chapter name consist solely of whitespace. Furthermore, the chapter name should respect the constraints imposed by the safe.
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/model/book.rb', line 345 def import_chapter( chapter_name, chapter_data ) KeyError.not_new( chapter_name, self ) raise RuntimeError, "The chapter must not be nil or empty." if( chapter_data.nil?() or chapter_data.empty?() ) chapter_exists = @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ].has_key?( chapter_name ) @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ][ chapter_name ] = DataStore.new() unless chapter_exists chapter_keys = @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ][ chapter_name ] new_chapter = Content.unlock_branch_chapter( chapter_keys ) if chapter_exists new_chapter = DataStore.new() unless chapter_exists new_chapter.merge_recursively!( chapter_data ) Content.lock_chapter( chapter_keys, new_chapter.to_json() ) end |
#init_time ⇒ String
Returns the human readable date/time denoting when the book was first initialized.
435 436 437 |
# File 'lib/model/book.rb', line 435 def init_time() return @book[ Indices::SAFE_BOOK_INITIALIZE_TIME ] end |
#init_version ⇒ String
Returns the safedb application software version at the time that the safe book was initialized.
464 465 466 |
# File 'lib/model/book.rb', line 464 def init_version() return @book[ Indices::SAFE_BOOK_INIT_VERSION ] end |
#is_open?(chapter_name, verse_name) ⇒ Boolean
Are both the chapter and verse names in the parameters open? An exception is thrown if any of the parameters are nil.
420 421 422 |
# File 'lib/model/book.rb', line 420 def is_open?( chapter_name, verse_name ) return ( is_open_chapter?( chapter_name ) and is_open_verse?( verse_name ) ) end |
#is_open_chapter?(this_chapter_name) ⇒ Boolean
Is the chapter name in the parameter the book’s open chapter? An exception is thrown if the parameter chapter name is nil.
372 373 374 375 376 |
# File 'lib/model/book.rb', line 372 def is_open_chapter?( this_chapter_name ) raise RuntimeError, "Cannot test a nil chapter name." if this_chapter_name.nil?() return false unless has_open_chapter_name?() return this_chapter_name.eql?( get_open_chapter_name() ) end |
#is_open_verse?(this_verse_name) ⇒ Boolean
Is the verse name in the parameter the book’s open verse? An exception is thrown if the parameter verse name is nil.
409 410 411 412 413 |
# File 'lib/model/book.rb', line 409 def is_open_verse?( this_verse_name ) raise RuntimeError, "Cannot test a nil verse name." if this_verse_name.nil?() return false unless has_open_verse_name?() return this_verse_name.eql?( get_open_verse_name() ) end |
#is_opened? ⇒ Boolean
Has this book been opened at a chapter and verse location.
427 428 429 |
# File 'lib/model/book.rb', line 427 def is_opened?() return has_open_chapter_name?() && has_open_verse_name?() end |
#print_book_mark ⇒ Object
Print a notie stating the book followed by and open chapter and verse names only if the book is currently opened at a specific chapter and verse. Right after book creation for example there is no open chapter or verse and this method simply prints out a carriage return.
393 394 395 396 397 398 399 400 401 402 |
# File 'lib/model/book.rb', line 393 def print_book_mark() puts "" return unless is_opened?() bcv_name = "#{book_name()}:#{get_open_chapter_name()}/#{get_open_verse_name()}" puts "#{bcv_name} (#{get_open_verse_data().length()})\n" puts "" end |
#read ⇒ String
Construct a Book object that extends the DataStore data structure which in turns extens the Ruby hash object. The parental objects know how to manipulate (store, delete, read etc the data structures).
To read the book index we first find the appropriate shell key and the appropriate book index ciphertext, one decrypts the other to produce the master database decryption key which in turn reveals the JSON representation of the master database.
The DataMap book index JSON is streamed into one of the crypt files denoted by a content identifier - this file is decrypted and the data structure deserialized into a Hash and returned.
Steps Taken To Read the Master Database
Reading up and returning the master database requires a rostra of actions namely
-
finding the branch data and reading the ID of the book in play
-
using the content id, branch id and book id to locate the crypt file
-
using the branch shell key and salt to unlock the content encryption key
-
using the content crypt key and random iv to unlock the file’s ciphertext
126 127 128 129 130 131 132 |
# File 'lib/model/book.rb', line 126 def read() read_crypt_path = FileTree.branch_crypts_filepath( @book_id, @branch_id, @content_id ) random_iv = KeyIV.in_binary( @branch_keys.get( Indices::CONTENT_RANDOM_IV ) ) @book = DataStore.from_json( Content.unlock_it( read_crypt_path, @crypt_key, random_iv ) ) end |
#set_master_chapter_keys ⇒ Object
Initializes the master book index chapter keys by using the @crypt_key along with the random iv and content id (read from the master indices) to decrypt the ciphertext in a master crypt file (found using the book id and content id).
Once the book index ciphertext is decrypted access the struct holding the chapter crypt keys, content ids and ivs which is against the Indices::SAFE_BOOK_CHAPTER_KEYS index.
477 478 479 480 481 482 483 484 485 |
# File 'lib/model/book.rb', line 477 def set_master_chapter_keys() random_iv = KeyIV.in_binary( @master_keys.get( Indices::CONTENT_RANDOM_IV ) ) content_id = @master_keys.get( Indices::CONTENT_IDENTIFIER ) master_index_filepath = FileTree.master_crypts_filepath( @book_id, content_id ) master_index = DataStore.from_json( Content.unlock_it( master_index_filepath, @crypt_key, random_iv ) ) @master_chapter_keys = master_index[ Indices::SAFE_BOOK_CHAPTER_KEYS ] end |
#set_open_chapter_data ⇒ Object
Persist the instantiated chapter data structure including all its verses. It doesn’t make sense to persist an empty data structure so an exception is raised in this circumstance. Nor can a data structure be persisted if no name is set for the open chapter.
282 283 284 285 286 |
# File 'lib/model/book.rb', line 282 def set_open_chapter_data() raise RuntimeError, "Cannot persist the data with no open chapter name." unless has_open_chapter_name?() raise RuntimeError, "Cannot persist a nil or empty data structure." if @open_chapter_data.nil?() or @open_chapter_data.empty?() Content.lock_chapter( get_open_chapter_keys(), @open_chapter_data.to_json() ) end |
#set_open_chapter_name(chapter_name) ⇒ Object
Sets the name of the chapter that this book is to be opened at. This method overwrites the currently open chapter name if there is one.
230 231 232 |
# File 'lib/model/book.rb', line 230 def set_open_chapter_name( chapter_name ) @book[ Indices::OPENED_CHAPTER_NAME ] = chapter_name end |
#set_open_verse_name(verse_name) ⇒ Object
Sets the name of the verse that this book is to be opened at. This method overwrites the currently open verse name if there is one.
238 239 240 |
# File 'lib/model/book.rb', line 238 def set_open_verse_name( verse_name ) @book[ Indices::OPENED_VERSE_NAME ] = verse_name end |
#to_branch_data ⇒ Object
Get the hash data structure representing the branch’s state. This state may or may not be equivalent to the current master state as gettable by the #to_master_data method.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/model/book.rb', line 75 def to_branch_data() @branch_data = {} @branch_verse_count = 0 branch_chapter_keys().each_pair do | chapter_name, chapter_keys | branch_chapter_data = Content.unlock_branch_chapter( chapter_keys ) @branch_data.store( chapter_name, branch_chapter_data ) @branch_verse_count += branch_chapter_data.length end return @branch_data end |
#to_master_data ⇒ Object
Get the hash data structure representing the master’s state. This state may or may not be equivalent to the current branch state as gettable by the #to_branch_data method.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/model/book.rb', line 49 def to_master_data() set_master_chapter_keys() @master_data = {} @master_verse_count = 0 @master_chapter_keys.each_pair do | chapter_name, chapter_indices | master_chapter_data = Content.unlock_master_chapter( @book_id, chapter_indices ) @master_data.store( chapter_name, master_chapter_data ) @master_verse_count += master_chapter_data.length() end return @master_data end |
#unopened_chapter_verse? ⇒ Boolean
Return true if this book has NOT been opened at a chapter and verse location. This method uses TextChunk.not_open_message to print out a helpful message detailing how to open a chapter and verse.
Note that an open chapter need not contain any data. The same goes for an open verse. In these cases the open_chapter and open_verse methods both return empty data structures.
184 185 186 187 188 |
# File 'lib/model/book.rb', line 184 def unopened_chapter_verse?() return false if( has_open_chapter_name?() and has_open_verse_name?() ) TextChunk.() return true end |
#write ⇒ Object
This write content behaviour takes the parameter content, encyrpts and encodes it using the index key, which is itself derived from the shell key unlocking the intra branch ciphertext. The crypted content is written to a file whose path is derviced by content_ciphertxt_file_from_domain_name.
Steps Taken To Write the Content
Writing the content requires a rostra of actions namely
-
deriving filepaths to both the breadcrumb and ciphertext files
-
creating a random iv and adding its base64 form to the breadcrumbs
-
using the shell token to derive the (unique to the) shell key
-
using the shell key and (intra) ciphertext to acquire the index key
-
using the index key and random iv to encrypt and encode the content
-
writing the resulting ciphertext to a file at the designated path
In with the new then out with the old
Once the new book index crypt file is written, the random iv and content id are overwritten with the new values, and the old book index crypt file is deleted.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/model/book.rb', line 157 def write() old_crypt_path = FileTree.branch_crypts_filepath( @book_id, @branch_id, @content_id ) @content_id = Identifier.get_random_identifier( Indices::CONTENT_ID_LENGTH ) @branch_keys.set( Indices::CONTENT_IDENTIFIER, @content_id ) write_crypt_path = FileTree.branch_crypts_filepath( @book_id, @branch_id, @content_id ) iv_base64 = KeyIV.new().for_storage() @branch_keys.set( Indices::CONTENT_RANDOM_IV, iv_base64 ) random_iv = KeyIV.in_binary( iv_base64 ) Content.lock_it( write_crypt_path, @crypt_key, random_iv, @book.to_json, TextChunk.crypt_header( @book_id ) ) File.delete( old_crypt_path ) if File.exists? old_crypt_path end |
#write_open_chapter ⇒ Object
Write data for the open chapter to the configured safe store. Naturally there must be an open chapter name and the open chapter data cannot be nil or empty otherwise this write will throw an exception.
315 316 317 318 |
# File 'lib/model/book.rb', line 315 def write_open_chapter() set_open_chapter_data() write() end |