safedb 0.3.1011 → 0.4.1002

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +56 -19
  3. data/README.md +15 -15
  4. data/Rakefile +7 -0
  5. data/bin/safe +2 -2
  6. data/lib/{interprete.rb → cli.rb} +168 -121
  7. data/lib/controller/admin/README.md +47 -0
  8. data/lib/controller/admin/access.rb +47 -0
  9. data/lib/controller/admin/checkin.rb +83 -0
  10. data/lib/controller/admin/checkout.rb +57 -0
  11. data/lib/controller/admin/diff.rb +75 -0
  12. data/lib/{usecase → controller/admin}/export.rb +15 -14
  13. data/lib/controller/admin/goto.rb +52 -0
  14. data/lib/controller/admin/import.rb +54 -0
  15. data/lib/controller/admin/init.rb +113 -0
  16. data/lib/controller/admin/login.rb +88 -0
  17. data/lib/{usecase → controller/admin}/logout.rb +0 -0
  18. data/lib/controller/admin/open.rb +39 -0
  19. data/lib/{usecase → controller/admin}/token.rb +2 -2
  20. data/lib/controller/admin/tree.md +54 -0
  21. data/lib/{usecase → controller/admin}/use.rb +0 -0
  22. data/lib/controller/admin/view.rb +61 -0
  23. data/lib/{usecase → controller/api}/docker/README.md +0 -0
  24. data/lib/{usecase → controller/api}/docker/docker.rb +1 -1
  25. data/lib/{usecase → controller/api}/jenkins/README.md +0 -0
  26. data/lib/{usecase → controller/api}/jenkins/jenkins.rb +1 -1
  27. data/lib/{usecase → controller/api}/terraform/README.md +1 -1
  28. data/lib/{usecase → controller/api}/terraform/terraform.rb +1 -1
  29. data/lib/{usecase → controller/api}/vpn/README.md +1 -1
  30. data/lib/{usecase → controller/api}/vpn/vpn.ini +0 -0
  31. data/lib/{usecase → controller/api}/vpn/vpn.rb +0 -0
  32. data/lib/{usecase → controller}/config/README.md +0 -0
  33. data/lib/{usecase → controller}/edit/README.md +0 -0
  34. data/lib/controller/edit/editverse.rb +48 -0
  35. data/lib/controller/edit/put.rb +35 -0
  36. data/lib/controller/edit/remove.rb +29 -0
  37. data/lib/{usecase/update/README.md → controller/edit/rename.md} +0 -0
  38. data/lib/{usecase → controller}/files/README.md +1 -1
  39. data/lib/controller/files/read.rb +36 -0
  40. data/lib/{usecase/files/eject.rb → controller/files/write.rb} +15 -20
  41. data/lib/{usecase → controller}/id.rb +0 -0
  42. data/lib/controller/query/print.rb +26 -0
  43. data/lib/controller/query/queryverse.rb +39 -0
  44. data/lib/controller/query/show.rb +50 -0
  45. data/lib/{session/require.gem.rb → controller/requirer.rb} +13 -9
  46. data/lib/{usecase → controller}/set.rb +4 -4
  47. data/lib/controller/usecase.rb +244 -0
  48. data/lib/{usecase → controller}/verse.rb +0 -0
  49. data/lib/{usecase → controller}/visit/README.md +0 -0
  50. data/lib/{usecase → controller}/visit/visit.rb +0 -0
  51. data/lib/factbase/facts.safedb.net.ini +7 -7
  52. data/lib/{keytools/key.docs.rb → model/README.md} +102 -66
  53. data/lib/model/book.rb +484 -0
  54. data/lib/model/branch.rb +48 -0
  55. data/lib/model/checkin.feature +33 -0
  56. data/lib/{configs/README.md → model/configs.md} +4 -4
  57. data/lib/model/content.rb +214 -0
  58. data/lib/model/indices.rb +132 -0
  59. data/lib/model/safe_tree.rb +51 -0
  60. data/lib/model/state.inspect.rb +221 -0
  61. data/lib/model/state.migrate.rb +334 -0
  62. data/lib/model/text_chunk.rb +68 -0
  63. data/lib/{extension → utils/extend}/array.rb +0 -0
  64. data/lib/{extension → utils/extend}/dir.rb +0 -0
  65. data/lib/{extension → utils/extend}/file.rb +0 -0
  66. data/lib/utils/extend/hash.rb +76 -0
  67. data/lib/{extension → utils/extend}/string.rb +6 -6
  68. data/lib/{session/fact.finder.rb → utils/facts/fact.rb} +0 -0
  69. data/lib/utils/identity/identifier.rb +356 -0
  70. data/lib/{keytools/key.ident.rb → utils/identity/machine.id.rb} +67 -4
  71. data/lib/utils/inspect/inspector.rb +81 -0
  72. data/lib/{keytools/kdf.bcrypt.rb → utils/kdfs/bcrypt.rb} +0 -0
  73. data/lib/{keytools → utils/kdfs}/kdf.api.rb +16 -16
  74. data/lib/{keytools/key.local.rb → utils/kdfs/kdfs.rb} +40 -40
  75. data/lib/{keytools/kdf.pbkdf2.rb → utils/kdfs/pbkdf2.rb} +0 -0
  76. data/lib/{keytools/kdf.scrypt.rb → utils/kdfs/scrypt.rb} +0 -0
  77. data/lib/{keytools → utils}/key.error.rb +2 -2
  78. data/lib/{keytools → utils}/key.pass.rb +2 -2
  79. data/lib/{keytools → utils/keys}/key.64.rb +0 -0
  80. data/lib/{keytools → utils/keys}/key.rb +6 -2
  81. data/lib/{keytools/key.iv.rb → utils/keys/random.iv.rb} +0 -0
  82. data/lib/{logging/gem.logging.rb → utils/logs/logger.rb} +6 -5
  83. data/lib/{keytools/key.pair.rb → utils/store/datamap.rb} +48 -30
  84. data/lib/{keytools/key.db.rb → utils/store/datastore.rb} +38 -104
  85. data/lib/utils/store/merge-boys-school.json +40 -0
  86. data/lib/utils/store/merge-girls-school.json +48 -0
  87. data/lib/utils/store/merge-merged-data.json +56 -0
  88. data/lib/utils/store/struct.rb +75 -0
  89. data/lib/utils/store/test-commands.sh +24 -0
  90. data/lib/{keytools/key.now.rb → utils/time/timestamp.rb} +32 -21
  91. data/lib/version.rb +1 -1
  92. metadata +86 -73
  93. data/lib/extension/hash.rb +0 -33
  94. data/lib/keytools/key.algo.rb +0 -109
  95. data/lib/keytools/key.api.rb +0 -1326
  96. data/lib/keytools/key.id.rb +0 -322
  97. data/lib/modules/cryptology/amalgam.rb +0 -70
  98. data/lib/modules/cryptology/engineer.rb +0 -99
  99. data/lib/modules/mappers/dictionary.rb +0 -288
  100. data/lib/session/time.stamp.rb +0 -340
  101. data/lib/session/user.home.rb +0 -49
  102. data/lib/usecase/cmd.rb +0 -471
  103. data/lib/usecase/edit/delete.rb +0 -46
  104. data/lib/usecase/files/file_me.rb +0 -78
  105. data/lib/usecase/files/read.rb +0 -169
  106. data/lib/usecase/files/write.rb +0 -89
  107. data/lib/usecase/goto.rb +0 -57
  108. data/lib/usecase/import.rb +0 -157
  109. data/lib/usecase/init.rb +0 -61
  110. data/lib/usecase/login.rb +0 -72
  111. data/lib/usecase/open.rb +0 -71
  112. data/lib/usecase/print.rb +0 -40
  113. data/lib/usecase/put.rb +0 -81
  114. data/lib/usecase/show.rb +0 -138
  115. data/lib/usecase/update/rename.rb +0 -180
  116. data/lib/usecase/view.rb +0 -71
File without changes
File without changes
File without changes
@@ -15,14 +15,14 @@ machine.key.id = machine.p4ssk3y
15
15
  time.stamp.id = domain.stamp
16
16
  user.secret.id = user.secret
17
17
 
18
- stamp.14 = rb>> OpenSession::Stamp.yyjjj_hhmm_sst
19
- stamp.23 = rb>> OpenSession::Stamp.yyjjj_hhmm_ss_nanosec
18
+ stamp.14 = rb>> OpenBranch::Stamp.yyjjj_hhmm_sst
19
+ stamp.23 = rb>> OpenBranch::Stamp.yyjjj_hhmm_ss_nanosec
20
20
 
21
21
  separator.a = %$os$%
22
22
 
23
23
  repo.name = material_data
24
24
  config.file = ops.workstation.directive.ini
25
- session.file = ops.session.configuration.ini
25
+ branch.file = ops.branch.configuration.ini
26
26
 
27
27
  prompt.1 = Enter a Robust Password
28
28
  prompt.2 = Re-enter that Password
@@ -30,9 +30,9 @@ prompt.2 = Re-enter that Password
30
30
 
31
31
  [open]
32
32
 
33
- open.name = session
33
+ open.name = branch
34
34
  open.idlen = rb>> 10
35
35
  open.keylen = rb>> 56
36
- open.idname = session.id
37
- open.keyname = session.key
38
- open.pathname = session.path
36
+ open.idname = branch.id
37
+ open.keyname = branch.key
38
+ open.pathname = branch.path
@@ -1,4 +1,98 @@
1
- #!/usr/bin/ruby
1
+
2
+
3
+ # Cycling Book Master and Branch State
4
+
5
+ Cycle cycles state indices and content crypt files to and from master and branches.
6
+ The need to cycle content occurs during
7
+
8
+ - <tt>initialization</tt> - a new master state box is created
9
+ - <tt>login</tt> - branch state is created that mirrors master
10
+ - <tt>checkin</tt> - transfers state from branch to master
11
+ - <tt>checkout</tt> - transfers state from master to branch
12
+
13
+
14
+ ## State Elements Transition Table
15
+
16
+ | Element | Located | Create Usecases | Updated by Usecases | Read by Usecases | Derived From |
17
+ |:------------ |:------------ |:----------------- |:------------------- |:----------------- |:----------------- |
18
+ | Key derived from human password | Master Indices File | Book Init | Every Login | Every Login | human password and pbkdf2 and bcrypt salts |
19
+ | Strong Random Index Crypt Key | Locked with Human and Branch keys | First created during book initialization | Updated on the very first book login after machine bootup | Read on logins and then all book query/edit use cases | random generator
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+ == Book Login
31
+
32
+ <tt>Derive the Old</tt>
33
+
34
+ The login use case is about <tt>re-generating the key from the password text and salts<tt>
35
+ and then accessing the old human crypt key and using it to unlock and access the current strong
36
+ random content encryption key. The old ciphertext protecting the book index is also acquired
37
+ and unlocked.
38
+
39
+ <tt>Generate the New</tt>
40
+
41
+ Another strong key is acquired and used to lock the book index. This strong key is itself
42
+ locked by the newly generated key (rederived from the source (human) key and the
43
+
44
+
45
+ Finding and rederiving the old produces the book index ciphertext which , spinning up a new one and deftly unlocking the master
46
+ database with the old and immediately locking it back up again with the new.
47
+
48
+ The login process also creates a new workspace consisting of
49
+ - a clone of the master content crypt files
50
+ - a new set of indices allowing for the acquisition of the new content key via a shell-based branch key
51
+ - a mirrored commit reference that allows commit (save) back to the master if it hasn't moved forward
52
+ - stating that subsequent commands are for this book and other branch books in play are to be set aside
53
+
54
+ The logout process destroys the breadcrumb route back to the re-acquisition of the
55
+ content encryption key via the shell key. It also deletes the branch crypts.
56
+
57
+ == Login Logout Stack Push Pop
58
+
59
+ The login/logout works like a stack push pop or like a nested structure. A login wrests
60
+ control away from the currently logged in book whilst a logout cedes control to the
61
+ book that was last in play.
62
+
63
+ <b>Login Recycles 3 things</b>
64
+
65
+ The three (3) things recycled by this login are
66
+
67
+ - the human key (sourced by putting the secret text through two key derivation functions)
68
+ - the content crypt key (sourced from a random 48 byte sequence)
69
+ - the content ciphertext (sourced by decrypting with the old and re-encrypting with the new)
70
+
71
+ Remember that the content crypt key is itself encrypted by two key entities.
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
2
96
 
3
97
  # The open key library generates keys, it stores their salts, it produces differing
4
98
  # representations of the keys (like base64 for storage and binary for encrypting).
@@ -54,10 +148,10 @@
54
148
  # reference string.
55
149
  #
56
150
  # KeyCycle KeyLifeCycle implements the merry go round that palms off
57
- # responsibility to the intra-session cycle and then back again
58
- # to ever rotary inter-session(ary) cycle.
151
+ # responsibility to the intra-branch cycle and then back again
152
+ # to ever rotary inter-branch(ary) cycle.
59
153
  ########### Maybe think of a method where we pass in
60
- ########### 2 secrets - 1 human and 1 55 random bytes (session)
154
+ ########### 2 secrets - 1 human and 1 55 random bytes (branch)
61
155
  ###########
62
156
  ########### 1 another 55 random key is created (the actual encryption key)
63
157
  ########### 2 then the above key is encrypted TWICE (2 diff salts and keys)
@@ -114,13 +208,13 @@
114
208
  # - command line apps do not store the key - they only store the salt
115
209
  # - both throw away the original password
116
210
  #
117
- # == One Key | One Session | One Crypt
211
+ # == One Key | One branch | One Crypt
118
212
  #
119
213
  # Command line apps use the derived key to <b>symmetrically encrypt and decrypt</b>
120
214
  # one and only one 48 character key and a new key is derived at the beginning
121
- # of every session.
215
+ # of every branch.
122
216
  #
123
- # At the end of the session <b>all material encrypted by the outgoing key</b>
217
+ # At the end of the branch <b>all material encrypted by the outgoing key</b>
124
218
  # is removed. This aggressive key rotation strategy leaves no stone unturned in
125
219
  # the quest for ultimate security.
126
220
  #
@@ -132,64 +226,6 @@
132
226
  #
133
227
  # - [1] it does not store the key nor does it store the password
134
228
  #
135
- # - [2] a new master key is generated for every session only to hold the master index file
229
+ # - [2] a new master key is generated for every branch only to hold the master index file
136
230
  #
137
231
  # - [3] it uses both <b>BCrypt</b> (Blowfish Crypt) and the indefatigable <b>PBKD2</b>
138
-
139
-
140
- # After a successful initialization, the application instance is linked to a keystore
141
- # whose contents are responsible for securing the application instance database.
142
- #
143
- # To ascertain what needs to be done to bridge the gap to full initialization the
144
- # app needs to know 3 things from the KeyApi. These things are
145
- #
146
- # - the ID of this app instance on the machine
147
- # - if a keystore been associated with this ID
148
- # - whether the keystore secures the app database
149
- #
150
- # The answers dictate the steps that need to be undertaken to bring the database of
151
- # the application instance under the secure wing of the KeyApi.
152
- #
153
- #
154
- # == 1. What is the App Instance ID on this Machine?
155
- #
156
- # The KeyApi uses the "just given" application reference and the machine environment to
157
- # respond with a <b>digested identifier</b> binding the application instance to the
158
- # present machine (workstation).
159
- #
160
- #
161
- # == 2. Has a Keystore been associated with this ID?
162
- #
163
- # The application's configuration manager is asked to find an associated KeyStore ID
164
- # mapped against the app/machine id garnered by question 1.
165
- #
166
- # <b>No it has not!</b>
167
- #
168
- # If <b>NO</b> then a KeyStore ID is acquired <b>either from the init command's parameter</b>,
169
- # or a <b>suitable default</b>. This new association between the app/machine ID and the
170
- # KeyStore ID is then stored so the answer next time will be <b>YES</b>.
171
- #
172
- # <b>Yes it has!</b>
173
- #
174
- # Great - we now submit the KeyStore ID to the KeyApi so that it may answer question 3.
175
- #
176
- #
177
- # == 3. Does the keystore secure the app instance database?
178
- #
179
- # For the KeyApi to answer, it needs the App's Instance ID and the KeyStore ID.
180
- #
181
- # <b>Not Yet!</b> Now <b>NO</b> means this application instance's database has not been
182
- # brought under the protection of the KeyApi's multi-layered security net. For this it
183
- # needs
184
- #
185
- # - the KeyStore ID
186
- # - the application instance reference
187
- # - the plaintext secret from which nothing of the host survives
188
- # - the current application database plaintext
189
- #
190
- # <b>Yes it does!</b> If the app db keys <b>have been instantiated</b> and the client app is
191
- # <b>sitting pretty</b> in possession of the database ciphertext, no more needs doing.
192
-
193
- module SafeDb
194
-
195
- end
data/lib/model/book.rb ADDED
@@ -0,0 +1,484 @@
1
+ #!/usr/bin/ruby
2
+
3
+ module SafeDb
4
+
5
+ # The book index is pretty much like the index at the front of a book!
6
+ #
7
+ # With a real book the index tells you the page number of a chapter. Our Book
8
+ # knows about and behaves with concepts like
9
+ #
10
+ # - the list of chapters in the book including the chapter names
11
+ # - attributes like book name, creation and last accessed dates
12
+ # - book scoped configuration directives and their values
13
+ # - chapter keys including file content ids and encryption keys
14
+ #
15
+ # Parental use cases in the background will use this index to encrypt, decrypt
16
+ # create, read, update and delete chapter encased credentials.
17
+ #
18
+ class Book
19
+
20
+
21
+ # Initialize the book index data structure from the branch state file
22
+ # and the current branch identifier.
23
+ #
24
+ # We assume that something else created the very first book index so we
25
+ # never check whether it exists, instead we assume that one does exist.
26
+ def initialize
27
+
28
+ @branch_id = Identifier.derive_branch_id( Branch.to_token() )
29
+ @branch_keys = DataMap.new( FileTree.branch_indices_filepath( @branch_id ) )
30
+ @book_id = @branch_keys.read( Indices::BRANCH_DATA, Indices::CURRENT_BRANCH_BOOK_ID )
31
+ @branch_keys.use( @book_id )
32
+ @content_id = @branch_keys.get( Indices::CONTENT_IDENTIFIER )
33
+
34
+ @master_keys = DataMap.new( Indices::MASTER_INDICES_FILEPATH )
35
+ @master_keys.use( @book_id )
36
+
37
+ intra_key_ciphertext = @branch_keys.get( Indices::CRYPT_CIPHER_TEXT )
38
+ intra_key = KeyDerivation.regenerate_shell_key( Branch.to_token() )
39
+ @crypt_key = intra_key.do_decrypt_key( intra_key_ciphertext )
40
+
41
+ read()
42
+
43
+ end
44
+
45
+
46
+ # Get the hash data structure representing the master's state. This state
47
+ # may or may not be equivalent to the current branch state as gettable by
48
+ # the {to_branch_data} method.
49
+ def to_master_data()
50
+
51
+ set_master_chapter_keys()
52
+ @master_data = {}
53
+ @master_verse_count = 0
54
+
55
+ @master_chapter_keys.each_pair do | chapter_name, chapter_indices |
56
+ master_chapter_data = Content.unlock_master_chapter( @book_id, chapter_indices )
57
+ @master_data.store( chapter_name, master_chapter_data )
58
+ @master_verse_count += master_chapter_data.length()
59
+ end
60
+
61
+ return @master_data
62
+
63
+ end
64
+
65
+ # Get the number of verses in the master's data structure.
66
+ # @return [Number] the number of verses in the master's data.
67
+ def get_master_verse_count()
68
+ return @master_verse_count
69
+ end
70
+
71
+ # Get the hash data structure representing the branch's state. This state
72
+ # may or may not be equivalent to the current master state as gettable by
73
+ # the {to_master_data} method.
74
+ def to_branch_data()
75
+
76
+ @branch_data = {}
77
+ @branch_verse_count = 0
78
+
79
+ branch_chapter_keys().each_pair do | chapter_name, chapter_keys |
80
+
81
+ branch_chapter_data = Content.unlock_branch_chapter( chapter_keys )
82
+ @branch_data.store( chapter_name, branch_chapter_data )
83
+ @branch_verse_count += branch_chapter_data.length
84
+
85
+ end
86
+
87
+ return @branch_data
88
+
89
+ end
90
+
91
+ # Get the number of verses in the branch's data structure.
92
+ # @return [Number] the number of verses in the branch's data.
93
+ def get_branch_verse_count()
94
+ return @branch_verse_count
95
+ end
96
+
97
+
98
+
99
+ # Construct a Book object that extends the DataStore data structure
100
+ # which in turns extens the Ruby hash object. The parental objects know
101
+ # how to manipulate (store, delete, read etc the data structures).
102
+ #
103
+ # To read the book index we first find the appropriate shell key and the
104
+ # appropriate book index ciphertext, one decrypts the other to produce the master
105
+ # database decryption key which in turn reveals the JSON representation of the
106
+ # master database.
107
+ #
108
+ # The {DataMap} book index JSON is streamed into one of the crypt files denoted by
109
+ # a content identifier - this file is decrypted and the data structure deserialized
110
+ # into a {Hash} and returned.
111
+ #
112
+ # <b>Steps Taken To Read the Master Database</b>
113
+ #
114
+ # Reading up and returning the master database requires a rostra of actions namely
115
+ #
116
+ # - finding the branch data and reading the ID of the book in play
117
+ # - using the content id, branch id and book id to locate the crypt file
118
+ # - using the branch shell key and salt to unlock the content encryption key
119
+ # - using the content crypt key and random iv to unlock the file's ciphertext
120
+ #
121
+ # @return [String]
122
+ # decode, decrypt and hen return the plain text content that was written
123
+ # to a file by the {write_content} method.
124
+ def read()
125
+
126
+ read_crypt_path = FileTree.branch_crypts_filepath( @book_id, @branch_id, @content_id )
127
+ random_iv = KeyIV.in_binary( @branch_keys.get( Indices::CONTENT_RANDOM_IV ) )
128
+ @book = DataStore.from_json( Content.unlock_it( read_crypt_path, @crypt_key, random_iv ) )
129
+
130
+ end
131
+
132
+
133
+ # This write content behaviour takes the parameter content, encyrpts and
134
+ # encodes it using the index key, which is itself derived from the shell
135
+ # key unlocking the intra branch ciphertext. The crypted content is
136
+ # written to a file whose path is derviced by {content_ciphertxt_file_from_domain_name}.
137
+ #
138
+ # <b>Steps Taken To Write the Content</b>
139
+ #
140
+ # Writing the content requires a rostra of actions namely
141
+ #
142
+ # - deriving filepaths to both the breadcrumb and ciphertext files
143
+ # - creating a random iv and adding its base64 form to the breadcrumbs
144
+ # - using the shell token to derive the (unique to the) shell key
145
+ # - using the shell key and (intra) ciphertext to acquire the index key
146
+ # - using the index key and random iv to encrypt and encode the content
147
+ # - writing the resulting ciphertext to a file at the designated path
148
+ #
149
+ # <b>In with the new then out with the old</b>
150
+ #
151
+ # Once the new book index crypt file is written, the random iv and
152
+ # content id are overwritten with the new values, and the old book
153
+ # index crypt file is deleted.
154
+ #
155
+ def write()
156
+
157
+ old_crypt_path = FileTree.branch_crypts_filepath( @book_id, @branch_id, @content_id )
158
+
159
+ @content_id = Identifier.get_random_identifier( Indices::CONTENT_ID_LENGTH )
160
+ @branch_keys.set( Indices::CONTENT_IDENTIFIER, @content_id )
161
+ write_crypt_path = FileTree.branch_crypts_filepath( @book_id, @branch_id, @content_id )
162
+
163
+ iv_base64 = KeyIV.new().for_storage()
164
+ @branch_keys.set( Indices::CONTENT_RANDOM_IV, iv_base64 )
165
+ random_iv = KeyIV.in_binary( iv_base64 )
166
+
167
+ Content.lock_it( write_crypt_path, @crypt_key, random_iv, @book.to_json, TextChunk.crypt_header( @book_id ) )
168
+ File.delete( old_crypt_path ) if File.exists? old_crypt_path
169
+
170
+ end
171
+
172
+
173
+ # Return true if this book has been opened at a chapter and verse location.
174
+ # This method uses {TextChunk.not_open_message} to print out a helpful message
175
+ # detailing how to open a chapter and verse.
176
+ #
177
+ # Note that an open chapter need not contain any data. The same goes for an
178
+ # open verse. In these cases the {open_chapter} and {open_verse} methods both
179
+ # return empty data structures.
180
+ def unopened_chapter_verse()
181
+ return if has_open_chapter_name?() and has_open_verse_name?()
182
+ TextChunk.not_open_message()
183
+ end
184
+
185
+
186
+ # Returns true if this book index has a chapter name specified to be the
187
+ # open chapter. True is returned even if the open chapter data structure
188
+ # is empty.
189
+ # @return [Boolean] true if an open chapter name has been set for this book
190
+ def has_open_chapter_name?()
191
+ return @book.has_key?( Indices::OPENED_CHAPTER_NAME )
192
+ end
193
+
194
+
195
+ # Returns true if this book index has a verse name specified to be the
196
+ # open verse. True is returned even if the open verse data structure is
197
+ # empty.
198
+ # @return [Boolean] true if an open verse name has been set for this book
199
+ def has_open_verse_name?()
200
+ return @book.has_key?( Indices::OPENED_VERSE_NAME )
201
+ end
202
+
203
+
204
+ # Returns the name of the chapter that this book has been opened at.
205
+ # If has_open_chapter_name?() returns false this method will throw an exception.
206
+ # @return [String] the name of the chapter that this book is opened at
207
+ def get_open_chapter_name()
208
+ raise RuntimeError, "No chapter has been opened." unless has_open_chapter_name?()
209
+ return @book[ Indices::OPENED_CHAPTER_NAME ]
210
+ end
211
+
212
+
213
+ # Returns the name of the verse that this book has been opened at.
214
+ # If has_open_verse_name?() returns false this method will throw an exception.
215
+ # @return [String] the name of the verse that this book is opened at
216
+ def get_open_verse_name()
217
+ raise RuntimeError, "No verse has been opened." unless has_open_verse_name?()
218
+ return @book[ Indices::OPENED_VERSE_NAME ]
219
+ end
220
+
221
+
222
+ # Sets the name of the chapter that this book is to be opened at. This method
223
+ # overwrites the currently open chapter name if there is one.
224
+ # @param chapter_name [String] the name of the chapter to open
225
+ def set_open_chapter_name( chapter_name )
226
+ @book[ Indices::OPENED_CHAPTER_NAME ] = chapter_name
227
+ end
228
+
229
+
230
+ # Sets the name of the verse that this book is to be opened at. This method
231
+ # overwrites the currently open verse name if there is one.
232
+ # @param verse_name [String] the name of the verse to open
233
+ def set_open_verse_name( verse_name )
234
+ @book[ Indices::OPENED_VERSE_NAME ] = verse_name
235
+ end
236
+
237
+
238
+ # Returns true if this book index has a chapter name specified to be the
239
+ # open chapter. True is returned even if the open chapter data structure
240
+ # is empty.
241
+ # @return [Boolean] true if an open chapter name has been set for this book
242
+ def has_open_chapter_data?()
243
+ raise RuntimeError, "Unopened chapter prevents data availability check." unless has_open_chapter_name?()
244
+ return @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ].has_key?( get_open_chapter_name() )
245
+ end
246
+
247
+
248
+ # If chapter keys exist for the open chapter this method returns them. If not,
249
+ # it creates an in-place empty map and returns that.
250
+ #
251
+ # If no chapter in this book has been opened, signalled by has_open_chapter_name?()
252
+ # an exception is thrown.
253
+ # @return [DataStore] the chapter keys for the chapter this book is opened at
254
+ def get_open_chapter_keys()
255
+ raise RuntimeError, "Cannot get chapter keys as no chapter is open." unless has_open_chapter_name?()
256
+ @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ][ get_open_chapter_name() ] = DataStore.new() unless has_open_chapter_data?()
257
+ return @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ][ get_open_chapter_name() ]
258
+ end
259
+
260
+
261
+ # Returns the data structure corresponding to the book's open chapter.
262
+ # If has_open_chapter_name?() returns false this method will throw an exception.
263
+ # @return [DataStore] the data of the chapter that this book is opened at
264
+ def get_open_chapter_data()
265
+ raise RuntimeError, "Cannot read data as no chapter is open." unless has_open_chapter_name?()
266
+ return @open_chapter_data unless @open_chapter_data.nil?()
267
+ @open_chapter_data = DataStore.new unless has_open_chapter_data?()
268
+ @open_chapter_data = Content.unlock_branch_chapter( get_open_chapter_keys() ) if has_open_chapter_data?()
269
+ return @open_chapter_data
270
+ end
271
+
272
+
273
+ # Persist the instantiated chapter data structure including all its verses.
274
+ # It doesn't make sense to persist an empty data structure so an exception is
275
+ # raised in this circumstance. Nor can a data structure be persisted if no name
276
+ # is set for the open chapter.
277
+ def set_open_chapter_data()
278
+ raise RuntimeError, "Cannot persist the data with no open chapter name." unless has_open_chapter_name?()
279
+ raise RuntimeError, "Cannot persist a nil or empty data structure." if @open_chapter_data.nil?() or @open_chapter_data.empty?()
280
+ Content.lock_chapter( get_open_chapter_keys(), @open_chapter_data.to_json() )
281
+ end
282
+
283
+
284
+ # Returns true if this book index has a verse name specified to be the
285
+ # open verse. True is returned even if the open verse data structure
286
+ # is empty.
287
+ # @return [Boolean] true if an open verse name has been set for this book
288
+ def has_open_verse_data?()
289
+ raise RuntimeError, "Cannot look for verse as no open chapter." unless has_open_chapter_name?()
290
+ raise RuntimeError, "Cannot look for verse as no open verse." unless has_open_verse_name?()
291
+ chapter_data = get_open_chapter_data()
292
+ return false if chapter_data.empty?()
293
+ return chapter_data.has_key?( get_open_verse_name() )
294
+ end
295
+
296
+
297
+ # Returns the data structure corresponding to the book's open verse within
298
+ # the book's open chapter. Both the open chapter and verse names have to be
299
+ # set otherwise an exception will be thrown.
300
+ # @return [DataStore] the data of the verse that this book is opened at
301
+ def get_open_verse_data()
302
+ get_open_chapter_data()[ get_open_verse_name() ] = DataStore.new() unless has_open_verse_data?()
303
+ return get_open_chapter_data()[ get_open_verse_name() ]
304
+ end
305
+
306
+
307
+ # Write data for the open chapter to the configured safe store. Naturally
308
+ # there must be an open chapter name and the open chapter data cannot be
309
+ # nil or empty otherwise this write will throw an exception.
310
+ def write_open_chapter()
311
+ set_open_chapter_data()
312
+ write()
313
+ end
314
+
315
+
316
+ # Import and persist the parameter data structure into this book with the
317
+ # parameter chapter name using a deep merge that recursively seeks to preserve
318
+ # all non-duplicate records in both the source and destination structures.
319
+ #
320
+ # <tt>Chapter Alreay Exists</tt>
321
+ #
322
+ # What if a chapter of the given name already exists?
323
+ #
324
+ # In this case we merge the new (incoming) chapter data into the old chapter
325
+ # data. Existing verses not declared in the incoming chapter data continue
326
+ # to live on.
327
+ #
328
+ # Duplicates occur when the same keys posses different values at any level
329
+ # including verse and line (even sub-line levels for assimilated files).The
330
+ # default is to favour incoming values when duplicates are encountered.
331
+ #
332
+ # <tt>Parameter Validation</tt>
333
+ #
334
+ # Neither of the parameters should be nil or empty,
335
+ # nor should the chapter name consist solely of whitespace. Furthermore,
336
+ # the chapter name should respect the constraints imposed by the safe.
337
+ #
338
+ # @param chapter_name [String] the name of the chapter to persist
339
+ # @param chapter_data [DataStore] the chapter data structure to persist
340
+ def import_chapter( chapter_name, chapter_data )
341
+
342
+ KeyError.not_new( chapter_name, self )
343
+ raise RuntimeError, "The chapter must not be nil or empty." if( chapter_data.nil?() or chapter_data.empty?() )
344
+
345
+ chapter_exists = @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ].has_key?( chapter_name )
346
+ @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ][ chapter_name ] = DataStore.new() unless chapter_exists
347
+ chapter_keys = @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ][ chapter_name ]
348
+ new_chapter = Content.unlock_branch_chapter( chapter_keys ) if chapter_exists
349
+ new_chapter = DataStore.new() unless chapter_exists
350
+
351
+ new_chapter.merge_recursively!( chapter_data )
352
+ Content.lock_chapter( chapter_keys, new_chapter.to_json() )
353
+
354
+ end
355
+
356
+
357
+ # Get the number of chapters nestled within this book.
358
+ # @return [Numeric] the number of chapters within this book
359
+ def chapter_count()
360
+ return branch_chapter_keys().length()
361
+ end
362
+
363
+
364
+ # Is the chapter name in the parameter the book's open chapter? An exception
365
+ # is thrown if the parameter chapter name is nil.
366
+ # @param this_chapter_name [String] the name of the chapter to test
367
+ def is_open_chapter?( this_chapter_name )
368
+ raise RuntimeError, "Cannot test a nil chapter name." if this_chapter_name.nil?()
369
+ return false unless has_open_chapter_name?()
370
+ return this_chapter_name.eql?( get_open_chapter_name() )
371
+ end
372
+
373
+
374
+ # Return true if the commit identifiers for the master and the branch match
375
+ # meaning that we can commit (checkin).
376
+ # @return [Boolean] true if can checkin, false otherwise
377
+ def can_checkin?()
378
+ return @branch_keys.get( Indices::COMMIT_IDENTIFIER ).eql?( @master_keys.get( Indices::COMMIT_IDENTIFIER ) )
379
+ end
380
+
381
+
382
+ def print_book_mark()
383
+ bcv_name = "#{book_name()}/#{get_open_chapter_name()}/#{get_open_verse_name()}"
384
+ puts ""
385
+ puts "#{bcv_name} (#{get_open_verse_data().length()})\n"
386
+ puts ""
387
+ end
388
+
389
+ # Is the verse name in the parameter the book's open verse? An exception
390
+ # is thrown if the parameter verse name is nil.
391
+ # @param this_verse_name [String] the name of the verse to test
392
+ def is_open_verse?( this_verse_name )
393
+ raise RuntimeError, "Cannot test a nil verse name." if this_verse_name.nil?()
394
+ return false unless has_open_verse_name?()
395
+ return this_verse_name.eql?( get_open_verse_name() )
396
+ end
397
+
398
+
399
+ # Are both the chapter and verse names in the parameters open? An exception
400
+ # is thrown if any of the parameters are nil.
401
+ # @param chapter_name [String] the name of the chapter to test
402
+ # @param verse_name [String] the name of the verse to test
403
+ def is_open?( chapter_name, verse_name )
404
+ return ( is_open_chapter?( chapter_name ) and is_open_verse?( verse_name ) )
405
+ end
406
+
407
+
408
+ # Has this book been opened at a chapter and verse location.
409
+ # @return [Boolean] true if it has an open chapter and an open verse
410
+ def is_opened?()
411
+ return has_open_chapter_name?() && has_open_verse_name?()
412
+ end
413
+
414
+
415
+ # Returns the human readable date/time denoting when the book was
416
+ # first initialized.
417
+ # @return [String] the time that this book was first initialized
418
+ def init_time()
419
+ return @book[ Indices::SAFE_BOOK_INITIALIZE_TIME ]
420
+ end
421
+
422
+
423
+ # Returns the name of the safe book.
424
+ # @return [String] the name of this book
425
+ def book_name()
426
+ return @book[ Indices::SAFE_BOOK_NAME ]
427
+ end
428
+
429
+
430
+ # Returns the id number of the safe book.
431
+ # @return [String] the id of this safe book
432
+ def book_id()
433
+ return @book_id
434
+ end
435
+
436
+
437
+ # Returns the id number of the current safe branch
438
+ # @return [String] the id of this safe branch
439
+ def branch_id()
440
+ return @branch_id
441
+ end
442
+
443
+
444
+ # Returns the safedb application software version at the time that the
445
+ # safe book was initialized.
446
+ # @return [String] the software version that initialized this book
447
+ def init_version()
448
+ return @book[ Indices::SAFE_BOOK_INIT_VERSION ]
449
+ end
450
+
451
+
452
+ # Initializes the master book index chapter keys by using the {@crypt_key}
453
+ # along with the random iv and content id (read from the master indices)
454
+ # to decrypt the ciphertext in a master crypt file (found using the book id
455
+ # and content id).
456
+ #
457
+ # Once the book index ciphertext is decrypted access the struct holding
458
+ # the chapter crypt keys, content ids and ivs which is against the
459
+ # {Indices::SAFE_BOOK_CHAPTER_KEYS} index.
460
+ def set_master_chapter_keys()
461
+
462
+ random_iv = KeyIV.in_binary( @master_keys.get( Indices::CONTENT_RANDOM_IV ) )
463
+ content_id = @master_keys.get( Indices::CONTENT_IDENTIFIER )
464
+ master_index_filepath = FileTree.master_crypts_filepath( @book_id, content_id )
465
+ master_index = DataStore.from_json( Content.unlock_it( master_index_filepath, @crypt_key, random_iv ) )
466
+ @master_chapter_keys = master_index[ Indices::SAFE_BOOK_CHAPTER_KEYS ]
467
+
468
+ end
469
+
470
+
471
+ # Returns a map of chapter keys that exist within the current branch
472
+ # (line) of this book.
473
+ # An empty map will be returned if no data has been added as yet
474
+ # to the book.
475
+ # @return [DataStore] the data structure holding chapter key data
476
+ def branch_chapter_keys()
477
+ return @book[ Indices::SAFE_BOOK_CHAPTER_KEYS ]
478
+ end
479
+
480
+
481
+ end
482
+
483
+
484
+ end