safedb 0.3.1011 → 0.4.1002
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +56 -19
- data/README.md +15 -15
- data/Rakefile +7 -0
- data/bin/safe +2 -2
- data/lib/{interprete.rb → cli.rb} +168 -121
- data/lib/controller/admin/README.md +47 -0
- data/lib/controller/admin/access.rb +47 -0
- data/lib/controller/admin/checkin.rb +83 -0
- data/lib/controller/admin/checkout.rb +57 -0
- data/lib/controller/admin/diff.rb +75 -0
- data/lib/{usecase → controller/admin}/export.rb +15 -14
- data/lib/controller/admin/goto.rb +52 -0
- data/lib/controller/admin/import.rb +54 -0
- data/lib/controller/admin/init.rb +113 -0
- data/lib/controller/admin/login.rb +88 -0
- data/lib/{usecase → controller/admin}/logout.rb +0 -0
- data/lib/controller/admin/open.rb +39 -0
- data/lib/{usecase → controller/admin}/token.rb +2 -2
- data/lib/controller/admin/tree.md +54 -0
- data/lib/{usecase → controller/admin}/use.rb +0 -0
- data/lib/controller/admin/view.rb +61 -0
- data/lib/{usecase → controller/api}/docker/README.md +0 -0
- data/lib/{usecase → controller/api}/docker/docker.rb +1 -1
- data/lib/{usecase → controller/api}/jenkins/README.md +0 -0
- data/lib/{usecase → controller/api}/jenkins/jenkins.rb +1 -1
- data/lib/{usecase → controller/api}/terraform/README.md +1 -1
- data/lib/{usecase → controller/api}/terraform/terraform.rb +1 -1
- data/lib/{usecase → controller/api}/vpn/README.md +1 -1
- data/lib/{usecase → controller/api}/vpn/vpn.ini +0 -0
- data/lib/{usecase → controller/api}/vpn/vpn.rb +0 -0
- data/lib/{usecase → controller}/config/README.md +0 -0
- data/lib/{usecase → controller}/edit/README.md +0 -0
- data/lib/controller/edit/editverse.rb +48 -0
- data/lib/controller/edit/put.rb +35 -0
- data/lib/controller/edit/remove.rb +29 -0
- data/lib/{usecase/update/README.md → controller/edit/rename.md} +0 -0
- data/lib/{usecase → controller}/files/README.md +1 -1
- data/lib/controller/files/read.rb +36 -0
- data/lib/{usecase/files/eject.rb → controller/files/write.rb} +15 -20
- data/lib/{usecase → controller}/id.rb +0 -0
- data/lib/controller/query/print.rb +26 -0
- data/lib/controller/query/queryverse.rb +39 -0
- data/lib/controller/query/show.rb +50 -0
- data/lib/{session/require.gem.rb → controller/requirer.rb} +13 -9
- data/lib/{usecase → controller}/set.rb +4 -4
- data/lib/controller/usecase.rb +244 -0
- data/lib/{usecase → controller}/verse.rb +0 -0
- data/lib/{usecase → controller}/visit/README.md +0 -0
- data/lib/{usecase → controller}/visit/visit.rb +0 -0
- data/lib/factbase/facts.safedb.net.ini +7 -7
- data/lib/{keytools/key.docs.rb → model/README.md} +102 -66
- data/lib/model/book.rb +484 -0
- data/lib/model/branch.rb +48 -0
- data/lib/model/checkin.feature +33 -0
- data/lib/{configs/README.md → model/configs.md} +4 -4
- data/lib/model/content.rb +214 -0
- data/lib/model/indices.rb +132 -0
- data/lib/model/safe_tree.rb +51 -0
- data/lib/model/state.inspect.rb +221 -0
- data/lib/model/state.migrate.rb +334 -0
- data/lib/model/text_chunk.rb +68 -0
- data/lib/{extension → utils/extend}/array.rb +0 -0
- data/lib/{extension → utils/extend}/dir.rb +0 -0
- data/lib/{extension → utils/extend}/file.rb +0 -0
- data/lib/utils/extend/hash.rb +76 -0
- data/lib/{extension → utils/extend}/string.rb +6 -6
- data/lib/{session/fact.finder.rb → utils/facts/fact.rb} +0 -0
- data/lib/utils/identity/identifier.rb +356 -0
- data/lib/{keytools/key.ident.rb → utils/identity/machine.id.rb} +67 -4
- data/lib/utils/inspect/inspector.rb +81 -0
- data/lib/{keytools/kdf.bcrypt.rb → utils/kdfs/bcrypt.rb} +0 -0
- data/lib/{keytools → utils/kdfs}/kdf.api.rb +16 -16
- data/lib/{keytools/key.local.rb → utils/kdfs/kdfs.rb} +40 -40
- data/lib/{keytools/kdf.pbkdf2.rb → utils/kdfs/pbkdf2.rb} +0 -0
- data/lib/{keytools/kdf.scrypt.rb → utils/kdfs/scrypt.rb} +0 -0
- data/lib/{keytools → utils}/key.error.rb +2 -2
- data/lib/{keytools → utils}/key.pass.rb +2 -2
- data/lib/{keytools → utils/keys}/key.64.rb +0 -0
- data/lib/{keytools → utils/keys}/key.rb +6 -2
- data/lib/{keytools/key.iv.rb → utils/keys/random.iv.rb} +0 -0
- data/lib/{logging/gem.logging.rb → utils/logs/logger.rb} +6 -5
- data/lib/{keytools/key.pair.rb → utils/store/datamap.rb} +48 -30
- data/lib/{keytools/key.db.rb → utils/store/datastore.rb} +38 -104
- data/lib/utils/store/merge-boys-school.json +40 -0
- data/lib/utils/store/merge-girls-school.json +48 -0
- data/lib/utils/store/merge-merged-data.json +56 -0
- data/lib/utils/store/struct.rb +75 -0
- data/lib/utils/store/test-commands.sh +24 -0
- data/lib/{keytools/key.now.rb → utils/time/timestamp.rb} +32 -21
- data/lib/version.rb +1 -1
- metadata +86 -73
- data/lib/extension/hash.rb +0 -33
- data/lib/keytools/key.algo.rb +0 -109
- data/lib/keytools/key.api.rb +0 -1326
- data/lib/keytools/key.id.rb +0 -322
- data/lib/modules/cryptology/amalgam.rb +0 -70
- data/lib/modules/cryptology/engineer.rb +0 -99
- data/lib/modules/mappers/dictionary.rb +0 -288
- data/lib/session/time.stamp.rb +0 -340
- data/lib/session/user.home.rb +0 -49
- data/lib/usecase/cmd.rb +0 -471
- data/lib/usecase/edit/delete.rb +0 -46
- data/lib/usecase/files/file_me.rb +0 -78
- data/lib/usecase/files/read.rb +0 -169
- data/lib/usecase/files/write.rb +0 -89
- data/lib/usecase/goto.rb +0 -57
- data/lib/usecase/import.rb +0 -157
- data/lib/usecase/init.rb +0 -61
- data/lib/usecase/login.rb +0 -72
- data/lib/usecase/open.rb +0 -71
- data/lib/usecase/print.rb +0 -40
- data/lib/usecase/put.rb +0 -81
- data/lib/usecase/show.rb +0 -138
- data/lib/usecase/update/rename.rb +0 -180
- 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
|