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
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>>
|
19
|
-
stamp.23 = rb>>
|
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
|
-
|
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 =
|
33
|
+
open.name = branch
|
34
34
|
open.idlen = rb>> 10
|
35
35
|
open.keylen = rb>> 56
|
36
|
-
open.idname =
|
37
|
-
open.keyname =
|
38
|
-
open.pathname =
|
36
|
+
open.idname = branch.id
|
37
|
+
open.keyname = branch.key
|
38
|
+
open.pathname = branch.path
|
@@ -1,4 +1,98 @@
|
|
1
|
-
|
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-
|
58
|
-
# to ever rotary inter-
|
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 (
|
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
|
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
|
215
|
+
# of every branch.
|
122
216
|
#
|
123
|
-
# At the end of the
|
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
|
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
|