Class: SafeDb::Book

Inherits:
Object
  • Object
show all
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

Constructor Details

#initializeBook

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_idString

Returns the id number of the safe book.

Returns:

  • (String)

    the id of this safe book



449
450
451
# File 'lib/model/book.rb', line 449

def book_id()
  return @book_id
end

#book_nameString

Returns the name of the safe book.

Returns:

  • (String)

    the name of this book



442
443
444
# File 'lib/model/book.rb', line 442

def book_name()
  return @book[ Indices::SAFE_BOOK_NAME ]
end

#branch_chapter_keysDataStore

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.

Returns:

  • (DataStore)

    the data structure holding chapter key data



493
494
495
# File 'lib/model/book.rb', line 493

def branch_chapter_keys()
  return @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ]
end

#branch_idString

Returns the id number of the current safe branch

Returns:

  • (String)

    the id of this 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).

Returns:

  • (Boolean)

    true if can commit, false otherwise



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_countNumeric

Get the number of chapters nestled within this book.

Returns:

  • (Numeric)

    the number of chapters 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_countNumber

Get the number of verses in the branch’s data structure.

Returns:

  • (Number)

    the number of verses in the branch’s data.



95
96
97
# File 'lib/model/book.rb', line 95

def get_branch_verse_count()
  return @branch_verse_count
end

#get_master_verse_countNumber

Get the number of verses in the master’s data structure.

Returns:

  • (Number)

    the number of verses in the master’s data.



67
68
69
# File 'lib/model/book.rb', line 67

def get_master_verse_count()
  return @master_verse_count
end

#get_open_chapter_dataDataStore

Returns the data structure corresponding to the book’s open chapter. If has_open_chapter_name?() returns false this method will throw an exception.

Returns:

  • (DataStore)

    the data of the chapter that this book is opened at

Raises:

  • (RuntimeError)


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_keysDataStore

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.

Returns:

  • (DataStore)

    the chapter keys for the chapter this book is opened at

Raises:

  • (RuntimeError)


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_nameString

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.

Returns:

  • (String)

    the name of the chapter that this book is opened at

Raises:

  • (RuntimeError)


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_dataDataStore

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.

Returns:

  • (DataStore)

    the data of the verse that this book is opened at



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_nameString

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.

Returns:

  • (String)

    the name of the verse that this book is opened at

Raises:

  • (RuntimeError)


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.

Returns:

  • (Boolean)

    true if an open chapter name has been set for this book

Raises:

  • (RuntimeError)


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.

Returns:

  • (Boolean)

    true if an open chapter name has been set for this book



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.

Returns:

  • (Boolean)

    true if an open verse name has been set for this book

Raises:

  • (RuntimeError)


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.

Returns:

  • (Boolean)

    true if an open verse name has been set for this book



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.

Parameters:

  • chapter_name (String)

    the name of the chapter to persist

  • chapter_data (DataStore)

    the chapter data structure to persist

Raises:

  • (RuntimeError)


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_timeString

Returns the human readable date/time denoting when the book was first initialized.

Returns:

  • (String)

    the time that this 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_versionString

Returns the safedb application software version at the time that the safe book was initialized.

Returns:

  • (String)

    the software version that initialized this book



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.

Parameters:

  • chapter_name (String)

    the name of the chapter to test

  • verse_name (String)

    the name of the verse to test

Returns:

  • (Boolean)


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.

Parameters:

  • this_chapter_name (String)

    the name of the chapter to test

Returns:

  • (Boolean)

Raises:

  • (RuntimeError)


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.

Parameters:

  • this_verse_name (String)

    the name of the verse to test

Returns:

  • (Boolean)

Raises:

  • (RuntimeError)


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.

Returns:

  • (Boolean)

    true if it has an open chapter and an open verse



427
428
429
# File 'lib/model/book.rb', line 427

def is_opened?()
  return has_open_chapter_name?() && has_open_verse_name?()
end

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

#readString

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

Returns:

  • (String)

    decode, decrypt and hen return the plain text content that was written to a file by the write_content method.



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_keysObject

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_dataObject

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.

Raises:

  • (RuntimeError)


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.

Parameters:

  • chapter_name (String)

    the name of the chapter to open



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.

Parameters:

  • verse_name (String)

    the name of the verse to open



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_dataObject

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_dataObject

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.

Returns:

  • (Boolean)

    true if no chapter and verse for this book is open



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.not_open_message()
  return true
end

#writeObject

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_chapterObject

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