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
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/ruby
2
+
3
+ module SafeDb
4
+
5
+ # This class knows the location of the main indices and crypt files
6
+ # and folders both for the master and branch lines.
7
+ #
8
+ # More importantly, it knows where the master crypts and indices are
9
+ # given a book id, and also the branch crypts and indices, given a
10
+ # branch id.
11
+ class FileTree
12
+
13
+
14
+ # Find the path to the file that contains the book index within the
15
+ # master (not branch) line. We need the book identifier and the file's
16
+ # content identifier to derive the path.
17
+ # @param book_id [String] the identifier of the book in question
18
+ # @param content_id [String] the identifier of the chapter content
19
+ # @return [File] path to the crypted content index file for book
20
+ def self.master_crypts_filepath( book_id, content_id )
21
+ return File.join( master_crypts_folder( book_id ), "safedb.chapter.#{content_id}.txt" )
22
+ end
23
+
24
+
25
+ def self.master_crypts_folder( book_id )
26
+ master_crypts_folder = File.join( Indices::SAFE_DATABASE_FOLDER, Indices::MASTER_CRYPTS_FOLDER_NAME )
27
+ return File.join( master_crypts_folder, "safedb.book.#{book_id}" )
28
+ end
29
+
30
+
31
+ def self.branch_crypts_filepath( book_id, branch_id, content_id )
32
+ return File.join( branch_crypts_folder( book_id, branch_id ), "safedb.chapter.#{content_id}.txt" )
33
+ end
34
+
35
+
36
+ def self.branch_crypts_folder( book_id, branch_id )
37
+ branch_crypts_folder = File.join( Indices::SAFE_DATABASE_FOLDER, Indices::BRANCH_CRYPTS_FOLDER_NAME )
38
+ return File.join( branch_crypts_folder, "safedb-branch-#{book_id}-#{branch_id}" )
39
+ end
40
+
41
+
42
+ def self.branch_indices_filepath( branch_id )
43
+ branch_indices_folder = File.join( Indices::SAFE_DATABASE_FOLDER, Indices::BRANCH_INDICES_FOLDER_NAME )
44
+ return File.join( branch_indices_folder, "safedb-indices-#{branch_id}.ini" )
45
+ end
46
+
47
+
48
+ end
49
+
50
+
51
+ end
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/ruby
2
+
3
+ module SafeDb
4
+
5
+ # State queries are related to {StateMigrate} but they simple ask for information
6
+ # about the state without changing any state.
7
+ #
8
+ class StateInspect
9
+
10
+
11
+ # A checkout is effectively an incoming merge of the master's data
12
+ # structure into the working branch. With checkouts nothing ever gets
13
+ # deleted.
14
+ #
15
+ # No delete is self-evident in this list of only <tt>4 prophetic</tt>
16
+ # outcomes
17
+ #
18
+ # - this chapter will be added
19
+ # - this verse will be added
20
+ # - this line will be added
21
+ # - this branch's line value will be overwritten with the value from master
22
+ #
23
+ # Examine the sister method {checkin_diff} that prophesizes on the
24
+ # state changes a checkin will invoke.
25
+ #
26
+ # @param master_data [Hash] data structure from the master line of the book
27
+ # @param branch_data [Hash] data structure from the current working branch
28
+ def self.checkout_prophecies( master_data, branch_data )
29
+
30
+ puts " = safe diff --checkout"
31
+ puts " = incoming from master to working branch"
32
+ puts ""
33
+
34
+ data_differences( master_data, branch_data )
35
+
36
+ end
37
+
38
+
39
+ # A checkout merges whilst a checkin is effectively a hard copy that destroys
40
+ # whatever is on the master making it exactly reflect the branch's current state.
41
+ #
42
+ # The three addition state changes prophesized by a checkout can also occur on
43
+ # checkins. However checkins can also prophesize that
44
+ #
45
+ # - this master's line value will be overwritten with the branch's value
46
+ # - this chapter will be removed
47
+ # - this verse will be removed
48
+ # - this line will be removed
49
+ #
50
+ # Examine the sister method {checkin_diff} that prophesizes on the
51
+ # state changes a checkin will invoke.
52
+ #
53
+ # @param master_data [Hash] data structure from the master line of the book
54
+ # @param branch_data [Hash] data structure from the current working branch
55
+ def self.checkin_prophecies( master_data, branch_data )
56
+
57
+ puts " = safe diff --checkin"
58
+ puts " = outgoing from working branch to master"
59
+ puts ""
60
+
61
+ data_differences( branch_data, master_data )
62
+ drop_differences( master_data, branch_data )
63
+
64
+ end
65
+
66
+
67
+
68
+ # Returns true if valid credentials have been provided earlier on in this
69
+ # session against the book specified in the parameter.
70
+ #
71
+ # Note the "in-use" concept. Even when specified book is not currently
72
+ # in use, true may be returned (as long as a successful login occured).
73
+ #
74
+ # @param book_id [String] book identifier that login request is against
75
+ # @return [Boolean] true if the parameter book is currently logged in
76
+ def self.is_logged_in?( book_id )
77
+
78
+ branch_id = Identifier.derive_branch_id( Branch.to_token() )
79
+ return false unless File.exists?( FileTree.branch_indices_filepath( branch_id ) )
80
+ branch_keys = DataMap.new( FileTree.branch_indices_filepath( branch_id ) )
81
+ return false unless branch_keys.has_section?( Indices::BRANCH_DATA )
82
+ return false unless branch_keys.has_section?( book_id )
83
+
84
+ branch_keys.use( book_id )
85
+ branch_key_ciphertext = branch_keys.get( Indices::CRYPT_CIPHER_TEXT )
86
+ branch_key = KeyDerivation.regenerate_shell_key( Branch.to_token() )
87
+
88
+ begin
89
+ branch_key.do_decrypt_key( branch_key_ciphertext )
90
+ return true
91
+ rescue OpenSSL::Cipher::CipherError => e
92
+ log.warn(x) { "A login check against book #{book_id} has failed." }
93
+ log.warn(x) { "Login failure error message is #{e.message}" }
94
+ return false
95
+ end
96
+
97
+ end
98
+
99
+
100
+ # Have any logins to this safe book occured since the machine was last
101
+ # rebooted? If no, true is returned. If another login has already occurred
102
+ # since the reboot false is returned.
103
+ #
104
+ # This method examines the bootup ID and if one exists and is equivalent
105
+ # to the current one, false is returned. Otherwise true is returned.
106
+ #
107
+ # Set the booup identifier within the parameter key/value map under the
108
+ # globally recognized {Indices::BOOTUP_IDENTIFIER} constant. This method
109
+ # expects the {DataMap} section name to be a significant identifier.
110
+ #
111
+ # @param data_map [DataMap] the data map in which we set the bootup id
112
+ # @return [Boolean] true if this is the first book login since bootup
113
+ def self.is_first_login?( data_map )
114
+
115
+ return true unless data_map.contains?( Indices::BOOTUP_IDENTIFIER )
116
+ old_bootup_id = data_map.get( Indices::BOOTUP_IDENTIFIER )
117
+ new_bootup_id = MachineId.get_bootup_id()
118
+ return old_bootup_id != new_bootup_id
119
+
120
+ end
121
+
122
+
123
+ private
124
+
125
+
126
+ def self.data_differences( this_data, that_data )
127
+
128
+ this_data.each_pair do | chapter_name, master_verse_data |
129
+
130
+ has_chapter = that_data.has_key?( chapter_name )
131
+ print_chapter_2b_added( chapter_name ) unless has_chapter
132
+ next unless has_chapter
133
+
134
+ branch_verse_data = that_data[ chapter_name ]
135
+ master_verse_data.each_pair do | verse_name, master_line_data |
136
+
137
+ has_verse = branch_verse_data.has_key?( verse_name )
138
+ print_verse_2_be_added( "#{chapter_name}/#{verse_name}" ) unless has_verse
139
+ next unless has_verse
140
+
141
+ branch_line_data = branch_verse_data[ verse_name ]
142
+ master_line_data.each_pair do | line_name, master_line_value |
143
+
144
+ has_line = branch_line_data.has_key?( line_name )
145
+ print_line_to_be_added( "#{chapter_name}/#{verse_name}/#{line_name}" ) unless has_line
146
+ next unless has_line
147
+
148
+ branch_line_value = branch_line_data[ line_name ]
149
+ lines_equal = master_line_value == branch_line_value
150
+ print_line_will_change( "#{chapter_name}/#{verse_name}/#{line_name}" ) unless lines_equal
151
+
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+
160
+ def self.drop_differences( this_data, that_data )
161
+
162
+ this_data.each_pair do | chapter_name, master_verse_data |
163
+
164
+ has_chapter = that_data.has_key?( chapter_name )
165
+ print_chapter_2b_removed( chapter_name ) unless has_chapter
166
+ next unless has_chapter
167
+
168
+ branch_verse_data = that_data[ chapter_name ]
169
+ master_verse_data.each_pair do | verse_name, master_line_data |
170
+
171
+ has_verse = branch_verse_data.has_key?( verse_name )
172
+ print_verse_2_be_removed( "#{chapter_name}/#{verse_name}" ) unless has_verse
173
+ next unless has_verse
174
+
175
+ branch_line_data = branch_verse_data[ verse_name ]
176
+ master_line_data.each_pair do | line_name, master_line_value |
177
+
178
+ has_line = branch_line_data.has_key?( line_name )
179
+ print_line_to_be_removed( "#{chapter_name}/#{verse_name}/#{line_name}" ) unless has_line
180
+
181
+ end
182
+
183
+ end
184
+
185
+ end
186
+
187
+ end
188
+
189
+ def self.print_chapter_2b_added( fq_chap_name )
190
+ puts " + Chapter 2b added -> #{fq_chap_name}"
191
+ end
192
+
193
+ def self.print_verse_2_be_added( fq_vers_name )
194
+ puts " + Verse 2 be added -> #{fq_vers_name}"
195
+ end
196
+
197
+ def self.print_line_to_be_added( fq_line_name )
198
+ puts " + Line to be added -> #{fq_line_name}"
199
+ end
200
+
201
+ def self.print_line_will_change( fq_line_name )
202
+ puts " + Line will change -> #{fq_line_name}"
203
+ end
204
+
205
+ def self.print_chapter_2b_removed( fq_chap_name )
206
+ puts " - Chapter 2b removed -> #{fq_chap_name}"
207
+ end
208
+
209
+ def self.print_verse_2_be_removed( fq_vers_name )
210
+ puts " - Verse 2 be removed -> #{fq_vers_name}"
211
+ end
212
+
213
+ def self.print_line_to_be_removed( fq_line_name )
214
+ puts " - Line to be removed -> #{fq_line_name}"
215
+ end
216
+
217
+
218
+ end
219
+
220
+
221
+ end
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/ruby
2
+
3
+ module SafeDb
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
+ class StateMigrate
14
+
15
+ # The login process recycles the content encryption key by regenerating the human
16
+ # key from the password text and salts and then accessing the old crypt key, generating
17
+ # the new one and deftly unlocking the master database with the old and immediately
18
+ # locking it back up again with the new.
19
+ #
20
+ # It also creates a new workspace of crypts and indices that initially mirror the current
21
+ # state of the master book. A login acts like a stack push in that it wrests control from
22
+ # the current book only to cede it back during logout.
23
+
24
+ # We recycle the (kdf) derived key every time we are handed the human password
25
+ # (during init and login) but the high entropy machine generated random key is
26
+ # only recycled at a special time.
27
+ #
28
+ # == When is the high entropy key recycled?
29
+ #
30
+ # The high entropy key is recycled only on the first login into a book since the
31
+ # machine reboot. This is because subsequent branch logins that protect the
32
+ # random key will need to check back with the master branch when performing either
33
+ # a diff or checkout operations. Also the checkin operation must maintain the
34
+ # same content encryption key for readability by validated agents.
35
+ #
36
+ # @param book_keys [DataMap]
37
+ # the {DataMap} contains the salts for key rederivation seeing as we have the
38
+ # book password and the rederived key will be able to unlock the ciphertext
39
+ # along with the random initialization vector (iv) also in the key map.
40
+ #
41
+ # Unlocking the ciphertext reveals the random high entropy key which can be
42
+ # used for the asymmetric decryption of the content ciphertext which is in a
43
+ # file marked with the content identifier also within the book keys.
44
+ #
45
+ # @param secret [String]
46
+ # the secret text that can potentially be cryptographically weak (low entropy).
47
+ # This text is severely strengthened and morphed into a key using multiple key
48
+ # derivation functions like <b>PBKDF2, BCrypt</b> and <b>SCrypt</b>.
49
+ #
50
+ # The secret text is discarded and the <b>derived inter-branch key</b> is used
51
+ # only to encrypt the <em>randomly generated super strong <b>index key</b></em>,
52
+ # <b>before being itself discarded</b>.
53
+ #
54
+ # The key ring only stores the salts. This means the secret text based key can
55
+ # only be regenerated at the next login, which explains the inter-branch label.
56
+ #
57
+ def self.login( book_keys, secret )
58
+
59
+ the_book_id = book_keys.section()
60
+
61
+ old_human_key = KdfApi.regenerate_from_salts( secret, book_keys )
62
+ the_crypt_key = old_human_key.do_decrypt_key( book_keys.get( Indices::CRYPT_CIPHER_TEXT ) )
63
+ plain_content = Content.unlock_master( the_crypt_key, book_keys )
64
+
65
+ first_login_since_boot = StateInspect.is_first_login?( book_keys )
66
+ the_crypt_key = Key.from_random if first_login_since_boot
67
+ recycle_keys( the_crypt_key, the_book_id, secret, book_keys, plain_content )
68
+ set_bootup_id( book_keys ) if first_login_since_boot
69
+
70
+ branch_id = Identifier.derive_branch_id( Branch.to_token() )
71
+ clone_book_into_branch( the_book_id, branch_id, book_keys, the_crypt_key )
72
+
73
+ end
74
+
75
+
76
+
77
+ # This method creates a new high entropy content encryption key and then forwards
78
+ # it on to behaviour that recycles the (kdf) key from the provided human sourced
79
+ # secret.
80
+ #
81
+ # @param book_id [String] the identifier of the book whose keys we are cycling
82
+ # @param human_secret [String] this secret is sourced into key derivation functions
83
+ # @param data_map [Hash] book related key/value data that will be populated as appropriate
84
+ # @param content_body [String] this content is encrypted and the ciphertext output stored
85
+ # @return [Key] the generated random high entropy key that the content is locked with
86
+ #
87
+ def self.recycle_both_keys( book_id, human_secret, data_map, content_body )
88
+ recycle_keys( Key.from_random(), book_id, human_secret, data_map, content_body )
89
+ end
90
+
91
+
92
+
93
+ # During initialization or login we recycle keys produced by key derivation
94
+ # functions (BCrypt. SCrypt and/or PBKDF2) from human sourced secrets.
95
+ #
96
+ # The flow of events of the recycling process is to
97
+ #
98
+ # - use the random high entropy key given in parameter one
99
+ # - lock the provided content with this high entropy key
100
+ # - save ciphertext in a file named by a random identifier
101
+ # - write this random identifier to the key cache
102
+ # - write the initialization vector to the key cache
103
+ # - use KDFs to derive a key from the human sourced password
104
+ # - save the salts crucial for reproducing this derived key
105
+ # - use the derived key to encrypt the high entropy key
106
+ # - write the resulting ciphertext into the key cache
107
+ # - return the high entropy key that locked the content
108
+ #
109
+ # @param high_entropy_key [Key] the machine generated high entropy content encryption key
110
+ # @param book_id [String] the identifier of the book whose keys we are cycling
111
+ # @param human_secret [String] this secret is sourced into key derivation functions
112
+ # @param data_map [Hash] book related key/value data that will be populated as appropriate
113
+ # @param content_body [String] this content is encrypted and the ciphertext output stored
114
+ def self.recycle_keys( high_entropy_key, book_id, human_secret, data_map, content_body )
115
+
116
+ Content.lock_master( book_id, high_entropy_key, data_map, content_body )
117
+ derived_key = KdfApi.generate_from_password( human_secret, data_map )
118
+ data_map.set( Indices::CRYPT_CIPHER_TEXT, derived_key.do_encrypt_key( high_entropy_key ) )
119
+
120
+ end
121
+
122
+
123
+ # In the main, the <tt>checkin use case</tt> changes the master so that it mirrors
124
+ # the branch's state. A check-in syncs the master's state to mirror the branch.
125
+ #
126
+ # == The Simple Check In
127
+ #
128
+ # The simplest case is when no other branch has issued a check-in since this branch
129
+ #
130
+ # - <tt>logged in</tt>
131
+ # - <tt>checked in</tt> or
132
+ # - <tt>checked out</tt>
133
+ #
134
+ # In this case the main events are to
135
+ #
136
+ # - make the master crypts mirror the branch crypts
137
+ # - update the master content ID to mirror the branch
138
+ # - give a new commit ID to both master and branch
139
+ #
140
+ # == The Commit ID Lifecycle
141
+ #
142
+ # A new commit ID is only created during
143
+ #
144
+ # - <tt>either the first login</tt> since the machine booted up
145
+ # - <tt>or a branch checkin</tt>
146
+ #
147
+ # The commit ID is copied from master to branch during
148
+ #
149
+ # - <tt>either subsequent logins</tt>
150
+ # - <tt>or a branch checkout</tt>
151
+ #
152
+ def self.checkin( book )
153
+
154
+ # @todo => If mismatch in commit IDs then print message instructing to first do safe checkout
155
+
156
+ FileUtils.remove_entry( FileTree.master_crypts_folder( book.book_id() ) )
157
+ FileUtils.mkdir_p( FileTree.master_crypts_folder( book.book_id() ) )
158
+ FileUtils.copy_entry( FileTree.branch_crypts_folder( book.book_id(), book.branch_id() ), FileTree.master_crypts_folder( book.book_id() ) )
159
+
160
+ master_keys = DataMap.new( Indices::MASTER_INDICES_FILEPATH )
161
+ master_keys.use( book.book_id() )
162
+ branch_keys = DataMap.new( FileTree.branch_indices_filepath( book.branch_id() ) )
163
+ branch_keys.use( book.book_id() )
164
+
165
+ checkin_commit_id = Identifier.get_random_identifier( 16 )
166
+ branch_keys.set( Indices::COMMIT_IDENTIFIER, checkin_commit_id )
167
+ master_keys.set( Indices::COMMIT_IDENTIFIER, checkin_commit_id )
168
+
169
+ master_keys.set( Indices::CONTENT_IDENTIFIER, branch_keys.get( Indices::CONTENT_IDENTIFIER ) )
170
+ master_keys.set( Indices::CONTENT_RANDOM_IV, branch_keys.get( Indices::CONTENT_RANDOM_IV ) )
171
+
172
+ end
173
+
174
+
175
+
176
+ # A checkout merges down the master's data into the data of this working branch.
177
+ # The <tt>commit ID</tt> of the working branch after the checkout is made to be
178
+ # equivalent with that of the master. This act signifies that a checkin is now
179
+ # allowed (as long as another branch doesn't checkin in the meantime).
180
+ #
181
+ # @param book [Book] the book whose master data will be merged down into the branch.
182
+ def self.checkout( book )
183
+
184
+ master_data = book.to_master_data()
185
+ branch_data = book.to_branch_data()
186
+
187
+ merged_verse_count = 0
188
+ master_data.each_pair do | chapter_name, chapter_data |
189
+ book.import_chapter( chapter_name, chapter_data )
190
+ merged_verse_count += chapter_data.length()
191
+ end
192
+
193
+ book.write()
194
+
195
+ puts ""
196
+ puts "#{master_data.length()} chapters and #{merged_verse_count} verses from master were merged in.\n"
197
+ puts ""
198
+
199
+ end
200
+
201
+
202
+ # Copy the master commit identifier to the branch. This signifies that the branch
203
+ # is aligned (and ready) to checkin its changes into the master.
204
+ # @param book [Book] the book whose commit IDs will be manipulated
205
+ def self.copy_commit_id_to_branch( book )
206
+
207
+ master_keys = DataMap.new( Indices::MASTER_INDICES_FILEPATH )
208
+ master_keys.use( book.book_id() )
209
+ branch_keys = DataMap.new( FileTree.branch_indices_filepath( book.branch_id() ) )
210
+ branch_keys.use( book.book_id() )
211
+
212
+ master_commit_id = master_keys.get( Indices::COMMIT_IDENTIFIER )
213
+ branch_keys.set( Indices::COMMIT_IDENTIFIER, master_commit_id )
214
+
215
+ end
216
+
217
+
218
+ # Set the booup identifier within the parameter key/value map under the
219
+ # globally recognized {Indices::BOOTUP_IDENTIFIER} constant. This method
220
+ # expects the {DataMap} section name to be a significant identifier.
221
+ #
222
+ # @param data_map [DataMap] the data map in which we set the bootup id
223
+ def self.set_bootup_id( data_map )
224
+
225
+ has_bootup_id = data_map.contains?( Indices::BOOTUP_IDENTIFIER )
226
+ old_bootup_id = data_map.get( Indices::BOOTUP_IDENTIFIER ) if has_bootup_id
227
+ log.info(x) { "overriding bootup id [#{old_bootup_id}] in section [#{data_map.section()}]." } if has_bootup_id
228
+
229
+ new_bootup_id = MachineId.get_bootup_id()
230
+ data_map.set( Indices::BOOTUP_IDENTIFIER, new_bootup_id )
231
+ log.info(x) { "setting bootup id in section [#{data_map.section()}] to [#{new_bootup_id}]." }
232
+ MachineId.log_reboot_times()
233
+
234
+ end
235
+
236
+
237
+ # Create the book within the master indices file and set its book identifier
238
+ # along with the initialize time and a fresh commit identifier.
239
+ #
240
+ # @param book_identifier [String] the identifier of the book to create
241
+ def self.create_book( book_identifier )
242
+ FileUtils.mkdir_p( FileTree.master_crypts_folder( book_identifier ) )
243
+
244
+ keypairs = DataMap.new( Indices::MASTER_INDICES_FILEPATH )
245
+ keypairs.use( book_identifier )
246
+ keypairs.set( Indices::SAFE_BOOK_INITIALIZE_TIME, KeyNow.readable() )
247
+ keypairs.set( Indices::COMMIT_IDENTIFIER, Identifier.get_random_identifier( 16 ) )
248
+ end
249
+
250
+
251
+ # Switch the current branch (if necessary) to using the book whose ID
252
+ # is specified in the parameter. Only call method if we are definitely
253
+ # in a logged in state.
254
+ #
255
+ # @param book_id [String] book identifier that login request is against
256
+ def self.use_book( book_id )
257
+ branch_id = Identifier.derive_branch_id( Branch.to_token() )
258
+ branch_keys = DataMap.new( FileTree.branch_indices_filepath( branch_id ) )
259
+ branch_keys.use( Indices::BRANCH_DATA )
260
+ current_book_id = branch_keys.get( Indices::CURRENT_BRANCH_BOOK_ID )
261
+ log.info(x) { "Current book is #{current_book_id} and the instruction is to use #{book_id}" }
262
+ branch_keys.set( Indices::CURRENT_BRANCH_BOOK_ID, book_id )
263
+ end
264
+
265
+
266
+ # When we login to a book which may or may not be the first book in the branch
267
+ # that we have logged into, we are effectively cloning all its master crypts and
268
+ # some of its keys (indices).
269
+ #
270
+ # To clone a book into a branch we
271
+ #
272
+ # - create a branch crypts folder and copy all master crypts into it
273
+ # - we create branch indices under general and book_id sections
274
+ # - we copy the commit reference and content identifier from the master
275
+ # - lock the content crypt key with the branch key and save the ciphertext
276
+ #
277
+ # == commit references
278
+ #
279
+ # We can only commit (save) a branch's crypts when the master and branch commit
280
+ # references match. The commit process places a new commit reference into both
281
+ # the master and branch indices. Like git's push/pull, this prevents a sync when
282
+ # the master has moved forward by one or more commits.
283
+ #
284
+ # @param book_id [String] the book identifier this branch is about
285
+ # @param branch_id [String] the identifier pertaining to this branch
286
+ # @param master_keys [DataMap] keys from the book's master line
287
+ # @param crypt_key [Key] symmetric branch content encryption key
288
+ #
289
+ def self.clone_book_into_branch( book_id, branch_id, master_keys, crypt_key )
290
+
291
+ FileUtils.mkdir_p( FileTree.branch_crypts_folder( book_id, branch_id ) )
292
+ FileUtils.copy_entry( FileTree.master_crypts_folder( book_id ), FileTree.branch_crypts_folder( book_id, branch_id ) )
293
+ branch_keys = create_branch_indices( book_id, branch_id )
294
+
295
+ branch_keys.set( Indices::CONTENT_IDENTIFIER, master_keys.get( Indices::CONTENT_IDENTIFIER ) )
296
+ branch_keys.set( Indices::CONTENT_RANDOM_IV, master_keys.get( Indices::CONTENT_RANDOM_IV ) )
297
+ branch_keys.set( Indices::COMMIT_IDENTIFIER, master_keys.get( Indices::COMMIT_IDENTIFIER ) )
298
+
299
+ branch_key = KeyDerivation.regenerate_shell_key( Branch.to_token() )
300
+ key_ciphertext = branch_key.do_encrypt_key( crypt_key )
301
+ branch_keys.set( Indices::CRYPT_CIPHER_TEXT, key_ciphertext )
302
+
303
+ end
304
+
305
+
306
+ # Create and return the branch indices {DataMap} pertaining to both the current
307
+ # book and branch whose ids are given in the first and second parameters.
308
+ #
309
+ # @param book_id [String] the book identifier this branch is about
310
+ # @param branch_id [String] the identifier pertaining to this branch
311
+ # @return [DataMap] return the keys pertaining to this branch and book
312
+ def self.create_branch_indices( book_id, branch_id )
313
+
314
+ branch_exists = File.exists? FileTree.branch_indices_filepath( branch_id )
315
+ branch_keys = DataMap.new( FileTree.branch_indices_filepath( branch_id ) )
316
+ branch_keys.use( Indices::BRANCH_DATA )
317
+ branch_keys.set( Indices::BRANCH_INITIAL_LOGIN_TIME, KeyNow.readable() ) unless branch_exists
318
+ branch_keys.set( Indices::BRANCH_LAST_ACCESSED_TIME, KeyNow.readable() )
319
+ branch_keys.set( Indices::CURRENT_BRANCH_BOOK_ID, book_id )
320
+
321
+ logged_in = branch_keys.has_section?( book_id )
322
+ branch_keys.use( book_id )
323
+ branch_keys.set( Indices::BOOK_BRANCH_LOGIN_TIME, KeyNow.readable() ) unless logged_in
324
+ branch_keys.set( Indices::BOOK_LAST_ACCESSED_TIME, KeyNow.readable() )
325
+
326
+ return branch_keys
327
+
328
+ end
329
+
330
+
331
+ end
332
+
333
+
334
+ end