opensecret 0.0.988 → 0.0.9925

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +56 -159
  3. data/bin/opensecret +2 -2
  4. data/bin/ops +17 -2
  5. data/lib/extension/string.rb +14 -16
  6. data/lib/{interpreter.rb → interprete.rb} +53 -29
  7. data/lib/keytools/binary.map.rb +49 -0
  8. data/lib/keytools/kdf.api.rb +249 -0
  9. data/lib/keytools/kdf.bcrypt.rb +64 -29
  10. data/lib/keytools/kdf.pbkdf2.rb +92 -83
  11. data/lib/keytools/kdf.scrypt.rb +190 -0
  12. data/lib/keytools/key.64.rb +326 -0
  13. data/lib/keytools/key.algo.rb +109 -0
  14. data/lib/keytools/key.api.rb +1281 -0
  15. data/lib/keytools/key.db.rb +265 -0
  16. data/lib/keytools/{key.module.rb → key.docs.rb} +55 -0
  17. data/lib/keytools/key.error.rb +110 -0
  18. data/lib/keytools/key.id.rb +271 -0
  19. data/lib/keytools/key.iv.rb +107 -0
  20. data/lib/keytools/key.local.rb +265 -0
  21. data/lib/keytools/key.mach.rb +248 -0
  22. data/lib/keytools/key.now.rb +402 -0
  23. data/lib/keytools/key.pair.rb +259 -0
  24. data/lib/keytools/key.pass.rb +120 -0
  25. data/lib/keytools/key.rb +428 -298
  26. data/lib/keytools/keydebug.txt +295 -0
  27. data/lib/logging/gem.logging.rb +3 -3
  28. data/lib/modules/cryptology/collect.rb +20 -0
  29. data/lib/session/require.gem.rb +1 -1
  30. data/lib/usecase/cmd.rb +417 -0
  31. data/lib/usecase/id.rb +36 -0
  32. data/lib/usecase/import.rb +174 -0
  33. data/lib/usecase/init.rb +78 -0
  34. data/lib/usecase/login.rb +70 -0
  35. data/lib/usecase/logout.rb +30 -0
  36. data/lib/usecase/open.rb +126 -0
  37. data/lib/{interprete → usecase}/put.rb +100 -47
  38. data/lib/usecase/read.rb +89 -0
  39. data/lib/{interprete → usecase}/safe.rb +0 -0
  40. data/lib/{interprete → usecase}/set.rb +0 -0
  41. data/lib/usecase/token.rb +111 -0
  42. data/lib/{interprete → usecase}/use.rb +0 -0
  43. data/lib/version.rb +1 -1
  44. data/opensecret.gemspec +4 -3
  45. metadata +39 -33
  46. data/lib/exception/cli.error.rb +0 -53
  47. data/lib/exception/errors/cli.errors.rb +0 -31
  48. data/lib/interprete/begin.rb +0 -232
  49. data/lib/interprete/cmd.rb +0 -621
  50. data/lib/interprete/export.rb +0 -163
  51. data/lib/interprete/init.rb +0 -205
  52. data/lib/interprete/key.rb +0 -119
  53. data/lib/interprete/open.rb +0 -148
  54. data/lib/interprete/seal.rb +0 -129
  55. data/lib/keytools/digester.rb +0 -245
  56. data/lib/keytools/key.data.rb +0 -227
  57. data/lib/keytools/key.derivation.rb +0 -341
  58. data/lib/modules/mappers/collateral.rb +0 -282
  59. data/lib/modules/mappers/envelope.rb +0 -127
  60. data/lib/modules/mappers/settings.rb +0 -170
  61. data/lib/notepad/scratch.pad.rb +0 -224
  62. data/lib/store-commands.txt +0 -180
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ module OpenKey
5
+
6
+ # Algorithms that are quality catalysts in the derivation and entropy spread
7
+ # of keys, identifiers and base64 character numbers.
8
+ class KeyAlgo
9
+
10
+
11
+ # Cherry pick a given number of characters from the character pool
12
+ # so that a good spread is achieved. This picker is the anti-pattern
13
+ # of just axing the first 5 characters from a 100 character string
14
+ # essentially wasting over 90% of the available entropy.
15
+ #
16
+ # This is the <b>algorithem to cherry pick</b> a spread of characters
17
+ # from the pool in the second parameter.
18
+ #
19
+ # - if the character pool length is a multiple of num_chars all is good otherwise
20
+ # - constrict to the <b>highest multiple of the pick size below</b> the pool length
21
+ # - divide that number by num_chars to get the first offset and character spacing
22
+ # - if spacing is 3, the first character is the 3rd, the second the 6th and so on
23
+ # - then return the cherry picked characters
24
+ #
25
+ # @param pick_size [FixNum] the number of characters to cherry pick
26
+ # @param char_pool [String] a pool of characters to cherry pick from
27
+ # @return [String]
28
+ # a string whose length is the one indicated by the first parameter
29
+ # and whose characters contain a predictable, repeatable spread from
30
+ # the character pool parameter
31
+ def self.cherry_picker( pick_size, char_pool )
32
+
33
+ hmb_limit = highest_multiple_below( pick_size, char_pool.length )
34
+ jump_size = hmb_limit / pick_size
35
+ read_point = jump_size
36
+ picked_chars = ""
37
+ loop do
38
+ picked_chars += char_pool[ read_point - 1 ]
39
+ read_point += jump_size
40
+ break if read_point > hmb_limit
41
+ end
42
+
43
+ err_msg = "Expected cherry pick size to be #{pick_size} but it was #{picked_chars.length}."
44
+ raise RuntimeError, err_msg unless picked_chars.length == pick_size
45
+
46
+ return picked_chars
47
+
48
+ end
49
+
50
+
51
+ # Affectionately known as <b>a hmb</b>, this method returns the
52
+ # <b>highest multiple</b> of the first parameter that is below
53
+ # <b>(either less than or equal to)</b> the second parameter.
54
+ #
55
+ # - -------- - ------- - ----------------- -
56
+ # | Small | Big | Highest Multiple |
57
+ # | Number | Number | Below Big Num |
58
+ # | -------- - ------- - ----------------- |
59
+ # | 5 | 25 | 25 |
60
+ # | 3 | 20 | 18 |
61
+ # | 8 | 63 | 56 |
62
+ # | 1 | 1 | 1 |
63
+ # | 26 | 28 | 26 |
64
+ # | 1 | 7 | 7 |
65
+ # | 16 | 16 | 16 |
66
+ # | -------- - ------- - ----------------- |
67
+ # | 10 | 8 | ERROR |
68
+ # | -4 | 17 | ERROR |
69
+ # | 4 | -17 | ERROR |
70
+ # | 0 | 32 | ERROR |
71
+ # | 29 | 0 | ERROR |
72
+ # | -4 | 0 | ERROR |
73
+ # | -------- - ------- - ----------------- |
74
+ # - -------- - ------- - ----------------- -
75
+ #
76
+ # Zeroes and negative numbers cannot be entertained, nor can the
77
+ # small number be larger than the big one.
78
+ #
79
+ # @param small_num [FixNum]
80
+ # the highest multiple of this number below the one in the
81
+ # next parameter is what will be returned.
82
+ #
83
+ # @param big_num [FixNum]
84
+ # returns either this number or the nearest below it that is
85
+ # a multiple of the number in the first parameter.
86
+ #
87
+ # @raise [ArgumentError]
88
+ # if the first parameter is greater than the second
89
+ # if either or both parameters are zero or negative
90
+ def self.highest_multiple_below small_num, big_num
91
+
92
+ arg_issue = (small_num > big_num) || small_num < 1 || big_num < 1
93
+ err_msg = "Invalid args #{small_num} and #{big_num} to HMB function."
94
+ raise ArgumentError, err_msg if arg_issue
95
+
96
+ for index in 0 .. ( big_num - 1 )
97
+ invex = big_num - index # an [invex] is an inverted index
98
+ return invex if invex % small_num == 0
99
+ end
100
+
101
+ raise ArgumentError, "Could not find a multiple of #{small_num} lower than #{big_num}"
102
+
103
+ end
104
+
105
+
106
+ end
107
+
108
+
109
+ end
@@ -0,0 +1,1281 @@
1
+ #!/usr/bin/ruby
2
+
3
+ module OpenKey
4
+
5
+ # Use the key applications programming interface to transition the
6
+ # state of three (3) core keys in accordance with the needs of the
7
+ # executing use case.
8
+ #
9
+ # == KeyApi | The 3 Keys
10
+ #
11
+ # The three keys service the needs of a <b>command line application</b>
12
+ # that executes within a <b>shell environment in a unix envirronment</b>
13
+ # or a <b>command prompt in windows</b>.
14
+ #
15
+ # So what are the 3 keys and what is their purpose.
16
+ #
17
+ # - shell key | exists to lock the index key created at login
18
+ # - human key | exists to lock the index key created at login
19
+ # - index key | exists to lock the application's index file
20
+ #
21
+ # So why do two keys (the shell key and human key) exist to lock the
22
+ # same index key?
23
+ #
24
+ # == KeyApi | Why Lock the Index Key Twice?
25
+ #
26
+ # On this login, the <b>previous login's human key is regenerated</b> from
27
+ # the <b>human password and the saved salts</b>. This <em>old human key</em>
28
+ # decrypts and reveals the <b><em>old index key</em></b> which in turn
29
+ # decrypts and reveals the index string.
30
+ #
31
+ # Both the old human key and the old index key are discarded.
32
+ #
33
+ # Then 48 bytes of randomness are sourced to generate the new index key. This
34
+ # key encrypts the now decrypted index string and is thrown away. The password
35
+ # sources a new human key (the salts are saved), and this new key locks the
36
+ # index key's source bytes.
37
+ #
38
+ # The shell key again locks the index key's source bytes. <b><em>Why twice?</em></b>
39
+ #
40
+ # - during subsequent shell command calls the human key is unavailable however
41
+ # the index key can be accessed via the shell key.
42
+ #
43
+ # - when the shell dies (or logout is issued) the shell key dies. Now the index
44
+ # key can only be accessed by a login when the password is made available.
45
+ #
46
+ # That is why the index key is locked twice. The shell key opens it mid-session
47
+ # and the regenerated human key opens it during the login of the next session.
48
+ #
49
+ # == The LifeCycle of each Key
50
+ #
51
+ # It seems odd that the human key is born during this login then dies
52
+ # at the very next one (as stated below). This is because the human key
53
+ # isn't the password, <b>the human key is sourced from the password</b>.
54
+ #
55
+ # So when are the 3 keys <b>born</b> and when do they <b>cease being</b>.
56
+ #
57
+ # - shell key | is born when the shell is created and dies when the shell dies
58
+ # - human key | is born when the user logs in this time and dies at the next login
59
+ # - index key | the life of the index key exactly mirrors that of the human key
60
+ #
61
+ # == The 7 Key API Calls
62
+ #
63
+ # | - | -------- | ------------ | ------------------------------- |
64
+ # | # | Rationale | Use Case | Goals | Tasks |
65
+ # | - | ------------------------------- | ------------ | ------------------------------- |
66
+ # | 1 | Create and Obfuscate Shell Key | key | x | y |
67
+ # | 2 | New App Instance on Workstation | init | x | y |
68
+ # | 3 | Login to App Instance in Shell | login | x | y |
69
+ #
70
+ class KeyApi
71
+
72
+
73
+ # This method should only be called once for each application instance
74
+ # resident on a workstation (machine) and it derives and writes the identifiers
75
+ # into the openkey configuration file.
76
+ #
77
+ # <b>The Identifiers to Configure</b>
78
+ #
79
+ # The principal identifiers to derive and configure are the
80
+ #
81
+ # - identifier for the application instance on this machine
82
+ # - global identifier derived for the application instance
83
+ # - keystore url location for this app on this machine
84
+ # - time the above two identifiers were burned to disk
85
+ #
86
+ # <b>Set(App) Configuration File</b>
87
+ #
88
+ # Neither the file nor its parent folder need to exist. We attempt to create
89
+ # the directory path and then the file. After this method has executed the
90
+ # below directives will be added to the openkey application coniguration.
91
+ #
92
+ # Config filepath is $HOME/.config/openkey/openkey.app.config.ini
93
+ #
94
+ # [srn1-apzd]
95
+ # app.instance.id = crnl-d3my
96
+ # keystore.url.id = /home/apollo/abcd/ab-motors-inc
97
+ # initialize.time = Fri May 25 11:59:46 2018 ( 18145.1159.462 )
98
+ #
99
+ # @param domain_name [String]
100
+ # the string reference that points to the application instance
101
+ # that is being initialized on this machine.
102
+ #
103
+ # @param keystore_url [String]
104
+ # The keystore url points to where the key metadata protecting
105
+ # this application instance lives. The simplest keystores are
106
+ # based on files and for them this url is just a folder path.
107
+ #
108
+ # The keystore URL cannot be <b>N.E.W</b> (nil, empty, whitespace only).
109
+ def self.init_app_domain( domain_name, keystore_url )
110
+
111
+ KeyError.not_new( domain_name, self )
112
+ KeyError.not_new( keystore_url, self )
113
+
114
+ aim_id = KeyId.derive_app_instance_machine_id( domain_name )
115
+ app_id = KeyId.derive_app_instance_identifier( domain_name )
116
+
117
+ keypairs = KeyPair.new( MACHINE_CONFIG_FILE )
118
+ keypairs.use( aim_id )
119
+ keypairs.set( APP_INSTANCE_ID_KEY, app_id )
120
+ keypairs.set( KEYSTORE_IDENTIFIER_KEY, keystore_url )
121
+ keypairs.set( APP_INITIALIZE_TIME, KeyNow.fetch() )
122
+
123
+ # --
124
+ # -- Switch the dominant application domain being used to
125
+ # -- the domain that is being initialized right here.
126
+ # --
127
+ use_application_domain( domain_name )
128
+
129
+ end
130
+
131
+
132
+ # Has the inter-sessionary key ( derived from a human secret ) been setup
133
+ # for the application shard referenced in the parameter?
134
+ #
135
+ # This method returns yes (true) if and only if
136
+ #
137
+ # - the application's keystore file exists
138
+ # - the file contains a breadcrumbs section
139
+ # - crumbs exist for human key rederivation
140
+ #
141
+ # If false return gives the go-ahead to
142
+ #
143
+ # - collect the human secret (in one of a number of ways)
144
+ # - pass it through key derivation functions
145
+ # - generate a high entropy power key and lock some initial content with it
146
+ # - use the key sourced from the human secret to lock the power key
147
+ # - throw away the secret, power key and human sourced key
148
+ # - save crumbs (ciphertext, salts, ivs) for content retrieval given secret
149
+ #
150
+ # Note that the {init_app_domain} method must have been called on this machine
151
+ # with the name of this application instance and the keystore url. An error results
152
+ # if no file is found at the {MACHINE_CONFIG_FILE} path.
153
+ #
154
+ # @param domain_name [String]
155
+ # a string reference for the <b>in-focus</b> shard of the application
156
+ #
157
+ # @return [Boolean]
158
+ # return true if the human secret for the parameter application name
159
+ # has been collected, transformed into a key, that key used to lock the
160
+ # power key, then secret and keys deleted, plus a trail of breadcrumbs
161
+ # sprinkled to allow the <b>inter-sessionary key to be regenerated</b>
162
+ # at the <b>next login</b>.
163
+ #
164
+ # <b>Lest we forget</b> - buried within this ensemble of activities, is
165
+ # <b>generating the high entropy power key</b>, using it to lock the
166
+ # application database before discarding it.
167
+ def self.is_domain_keys_setup?( domain_name )
168
+
169
+ KeyError.not_new( domain_name, self )
170
+ keypairs = KeyPair.new( MACHINE_CONFIG_FILE )
171
+ aim_id = KeyId.derive_app_instance_machine_id( domain_name )
172
+ app_id = KeyId.derive_app_instance_identifier( domain_name )
173
+ keypairs.use( aim_id )
174
+
175
+ keystore_file = get_keystore_file_from_domain_name( domain_name )
176
+ return false unless File.exists?( keystore_file )
177
+
178
+ crumbs_db = KeyPair.new( keystore_file )
179
+ return false unless crumbs_db.has_section?( APP_KEY_DB_BREAD_CRUMBS )
180
+
181
+ crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
182
+ return crumbs_db.contains?( INTER_KEY_CIPHERTEXT )
183
+
184
+ end
185
+
186
+
187
+ # Transform the domain secret into a key, use that key to lock the
188
+ # power key, delete the secret and keys and leave behind a trail of
189
+ # <b>breadcrumbs sprinkled</b> to allow the <b>inter-sessionary key</b>
190
+ # to be <b>regenerated</b> at the <b>next login</b>.
191
+ #
192
+ # <b>Lest we forget</b> - buried within this ensemble of activities, is
193
+ # <b>generating the high entropy power key</b>, using it to lock the
194
+ # application database before discarding it.
195
+ #
196
+ # The use case steps once the human secret is acquired is to
197
+ #
198
+ # - pass it through key derivation functions
199
+ # - generate a high entropy power key and lock some initial content with it
200
+ # - use the key sourced from the human secret to lock the power key
201
+ # - throw away the secret, power key and human sourced key
202
+ # - save crumbs (ciphertext, salts, ivs) for content retrieval given secret
203
+ #
204
+ # Note that the {init_app_domain} method must have been called on this machine
205
+ # with the name of this application instance and the keystore url. An error results
206
+ # if no file is found at the {MACHINE_CONFIG_FILE} path.
207
+ #
208
+ # @param domain_name [String]
209
+ # the string reference that points to the application instance
210
+ # that is being initialized on this machine.
211
+ #
212
+ # @param domain_secret [String]
213
+ # the secret text that can potentially be cryptographically weak (low entropy).
214
+ # This text is severely strengthened and morphed into a key using multiple key
215
+ # derivation functions like <b>PBKDF2, BCrypt</b> and <b>SCrypt</b>.
216
+ #
217
+ # The secret text is discarded and the <b>derived inter-session key</b> is used
218
+ # only to encrypt the <em>randomly generated super strong <b>index key</b></em>,
219
+ # <b>before being itself discarded</b>.
220
+ #
221
+ # @param content_header [String]
222
+ # the content header tops the ciphertext storage file with details of how where
223
+ # and why the file came to be.
224
+ def self.setup_domain_keys( domain_name, domain_secret, content_header )
225
+
226
+ # --
227
+ # -- Get the breadcrumbs trail and
228
+ # -- timestamp the moment.
229
+ # --
230
+ crumbs_db = get_crumbs_db_from_domain_name( domain_name )
231
+ crumbs_db.set( APP_INSTANCE_SETUP_TIME, KeyNow.fetch() )
232
+
233
+ # --
234
+ # -- Create a new power key and lock the content with it.
235
+ # -- Create a new inter key and lock the power key with it.
236
+ # -- Leave the necessary breadcrumbs for regeneration.
237
+ # --
238
+ recycle_keys( domain_name, domain_secret, crumbs_db, content_header, get_virgin_content( domain_name ) )
239
+
240
+ end
241
+
242
+
243
+ # Recycle the inter-sessionary key (based on the secret) and create a new
244
+ # content encryption (power) key and lock the parameter content with it
245
+ # before returning the new content encryption key.
246
+ #
247
+ # The {content_ciphertxt_file_from_domain_name} method is used to produce the path at which
248
+ # the ciphertext (resulting from locking the parameter content), is stored.
249
+ #
250
+ # @param domain_name [String]
251
+ #
252
+ # the (application instance) domain name chosen by the user or the
253
+ # machine that is interacting with the OpenKey software.
254
+ #
255
+ # @param domain_secret [String]
256
+ #
257
+ # the domain secret that is put through key derivation functions in order
258
+ # to attain the strongest possible inter-sessionary key which is used only
259
+ # to encrypt and decrypt the high-entropy content encryption key.
260
+ #
261
+ # @param crumbs_db [KeyPair]
262
+ #
263
+ # The crumbs database is expected to be initialized with a section
264
+ # ready to receive breadcrumb data. The crumbs data injected are
265
+ #
266
+ # - a random iv for future AES decryption of the parameter content
267
+ # - cryptographic salts for future rederivation of the inter-sessionary key
268
+ # - the resultant ciphertext from the inter key locking the content key
269
+ #
270
+ # @param the_content [String]
271
+ #
272
+ # the app database content whose ciphertext is to be recycled using the
273
+ # recycled (newly derived) high entropy random content encryption key.
274
+ def self.recycle_keys( domain_name, domain_secret, crumbs_db, content_header, the_content )
275
+
276
+ KeyError.not_new( domain_name, self )
277
+ KeyError.not_new( domain_secret, self )
278
+ KeyError.not_new( the_content, self )
279
+
280
+ # --
281
+ # -- Create a random initialization vector (iv)
282
+ # -- used for AES encryption of virgin content
283
+ # --
284
+ iv_base64_chars = KeyIV.new().for_storage()
285
+ crumbs_db.set( INDEX_DB_CRYPT_IV_KEY, iv_base64_chars )
286
+ random_iv = KeyIV.in_binary( iv_base64_chars )
287
+
288
+ # --
289
+ # -- Create a new high entropy power key
290
+ # -- for encrypting the virgin content.
291
+ # --
292
+ power_key = Key.from_random
293
+
294
+ # --
295
+ # -- Encrypt the virgin content using the
296
+ # -- power key and the random iv and write
297
+ # -- the Base64 encoded ciphertext into a
298
+ # -- neighbouring file.
299
+ # --
300
+ to_filepath = content_ciphertxt_file_from_domain_name( domain_name )
301
+ binary_ciphertext = power_key.do_encrypt_text( random_iv, the_content )
302
+ binary_to_write( to_filepath, content_header, binary_ciphertext )
303
+
304
+ # --
305
+ # -- Derive new inter-sessionary key.
306
+ # -- Use it to encrypt the power key.
307
+ # -- Set the reretrieval breadcrumbs.
308
+ # --
309
+ inter_key = KdfApi.generate_from_password( domain_secret, crumbs_db )
310
+ inter_txt = inter_key.do_encrypt_key( power_key )
311
+ crumbs_db.set( INTER_KEY_CIPHERTEXT, inter_txt )
312
+
313
+ # --
314
+ # -- Return the just createdC high entropy
315
+ # -- content encryption (power) key.
316
+ # --
317
+ return power_key
318
+
319
+ end
320
+
321
+
322
+
323
+ # At <b>the end</b> of a successful login the <b>old content crypt key</b> will
324
+ # have been <b>re-acquired and discarded,</b> with a <b>fresh one created</b>and
325
+ # put to work <b>protecting</b> the application's content.
326
+ #
327
+ # After reacquisitioning (but before discarding) the old crypt key, the app's
328
+ # key-value database is <b>silently decrypted with it then immediately re-encrypted</b>
329
+ # with the newly created (and locked down) crypt key.
330
+ #
331
+ # <b>Login Recycles 3 things</b>
332
+ #
333
+ # The three (3) things recycled by this login are
334
+ #
335
+ # - the human key (sourced by putting the secret text through two key derivation functions)
336
+ # - the content crypt key (sourced from a random 48 byte sequence)
337
+ # - the content ciphertext (sourced by decrypting with the old and re-encrypting with the new)
338
+ #
339
+ # Remember that the content crypt key is itself encrypted by two key entities.
340
+ #
341
+ # <b>The Inter and Intra Session Crypt Keys</b>
342
+ #
343
+ # This <b>login use case</b> is the <b>only time</b> in the session that the
344
+ # <b>human provided secret</b> is made available - hence the inter-session name.
345
+ #
346
+ # The intra session key is employed by use case calls on within (intra) the
347
+ # session it was created within.
348
+ #
349
+ # <b>The Weakness of the Human Inter Sessionary Key</b>
350
+ #
351
+ # The weakest link in the human-sourced key is clearly the human. Yes it is
352
+ # strengthened by key derivation functions with cost parameters as high is
353
+ # tolerable, but despite and in spite of these efforts, poorly chosen short
354
+ # passwords are not infeasible to acquire through brute force.
355
+ #
356
+ # The fallability is countered by invalidating and recycling the (inter session)
357
+ # key on every login, thus reducing the time frame available to an attacker.
358
+ #
359
+ # <b>The Weakness of the Shell Intra Sessionary Key</b>
360
+ #
361
+ # The shell key hails from a super random (infeasible to crack) source of
362
+ # 48 binary bytes. So what is its achilles heel?
363
+ #
364
+ # The means of protecting the shell key is the weakness. The source of its
365
+ # protection key is a motley crue of data unique not just to the workstation,
366
+ # but the parent shell. This is also passed through key derivation functions
367
+ # to strengthen it.
368
+ #
369
+ # <em><b>Temporary Environment Variables</b></em>
370
+ #
371
+ # The shell key's ciphertext lives as a short term environment variable so
372
+ # <b>when the shell dies the ciphertext dies</b> and any opportunity to resurrect
373
+ # the shell key <b>dies with it</b>.
374
+ #
375
+ # A <b>logout</b> command <b>removes the random iv and ciphertext</b> forged
376
+ # when the shell acted to encrypt the content key. Even mid shell session, a
377
+ # logout renders the shell key worthless.
378
+ #
379
+ # <b>Which (BreadCrumbs) endure?</b>
380
+ #
381
+ # Only <b>4 things endure</b> post the <b>login (recycle)</b> activities.
382
+ # These are the
383
+ #
384
+ # - salts and iteration counts used to generate the inter-session key
385
+ # - index key ciphertext after encryption using the inter-session key
386
+ # - index key ciphertext after encryption using the intra-session key
387
+ # - <b>content ciphertext</b> after the decrypt re-encrypt activities
388
+ #
389
+ #
390
+ # @param domain_name [String]
391
+ # the string reference that points to the application instance
392
+ # that is being initialized on this machine.
393
+ #
394
+ # @param domain_secret [String]
395
+ # the secret text that can potentially be cryptographically weak (low entropy).
396
+ # This text is severely strengthened and morphed into a key using multiple key
397
+ # derivation functions like <b>PBKDF2, BCrypt</b> and <b>SCrypt</b>.
398
+ #
399
+ # The secret text is discarded and the <b>derived inter-session key</b> is used
400
+ # only to encrypt the <em>randomly generated super strong <b>index key</b></em>,
401
+ # <b>before being itself discarded</b>.
402
+ #
403
+ # The key ring only stores the salts. This means the secret text based key can
404
+ # only be regenerated at the next login, which explains the inter-session label.
405
+ #
406
+ # <b>Note on Password Key Derivation</b>
407
+ # For each guess, a brute force attacker would need to perform
408
+ # <b>one million PBKDF2</b> and <b>65,536 BCrypt</b> algorithm
409
+ # iterations.
410
+ #
411
+ # Even so, a password of 6 characters or less can be successfully
412
+ # attacked. With all earth's computing resources working exclusively
413
+ # and in concert on attacking one password, it would take over
414
+ # <b>one million years to access the key</b> derived from a well spread
415
+ # 24 character password. And the key becomes obsolete the next time
416
+ # you login.
417
+ #
418
+ # Use the above information to decide on secrets with sufficient
419
+ # entropy and spread with at least 12 characters.
420
+ #
421
+ # @param content_header [String]
422
+ # the content header tops the ciphertext storage file with details of how where
423
+ # and why the file came to be.
424
+ def self.do_login( domain_name, domain_secret, content_header )
425
+
426
+ # --
427
+ # -- Get the breadcrumbs trail.
428
+ # --
429
+ crumbs_db = get_crumbs_db_from_domain_name( domain_name )
430
+
431
+ # --
432
+ # -- Get the old inter-sessionary key (created during the previous login)
433
+ # -- Get the old content encryption (power) key (again created during the previous login)
434
+ # -- Get the old random initialization vector (created during the previous login)
435
+ # --
436
+ old_inter_key = KdfApi.regenerate_from_salts( domain_secret, crumbs_db )
437
+ old_power_key = old_inter_key.do_decrypt_key( crumbs_db.get( INTER_KEY_CIPHERTEXT ) )
438
+ old_random_iv = KeyIV.in_binary( crumbs_db.get( INDEX_DB_CRYPT_IV_KEY ) )
439
+
440
+ # --
441
+ # -- Read the binary text representing the encrypted content
442
+ # -- that was last written by any use case capable of changing
443
+ # -- the application database content.
444
+ # --
445
+ from_filepath = content_ciphertxt_file_from_domain_name( domain_name )
446
+ old_crypt_txt = binary_from_read( from_filepath )
447
+
448
+ # --
449
+ # -- Decrypt the binary ciphertext that was last written by a use case
450
+ # -- capable of changing the application database.
451
+ # --
452
+ plain_content = old_power_key.do_decrypt_text( old_random_iv, old_crypt_txt )
453
+
454
+ # --
455
+ # -- Create a new power key and lock the content with it.
456
+ # -- Create a new inter key and lock the power key with it.
457
+ # -- Leave the necessary breadcrumbs for regeneration.
458
+ # -- Return the new power key that re-locked the content.
459
+ # --
460
+ power_key = recycle_keys( domain_name, domain_secret, crumbs_db, content_header, plain_content )
461
+
462
+ # --
463
+ # -- Regenerate intra-session key from the session token.
464
+ # -- Encrypt power key for intra (in) session retrieval.
465
+ # --
466
+ intra_key = KeyLocal.regenerate_shell_key( to_token() )
467
+ intra_txt = intra_key.do_encrypt_key( power_key )
468
+
469
+ # --
470
+ # -- Set the (ciphertext) breadcrumbs for re-acquiring the
471
+ # -- content encryption (power) key during (inside) this
472
+ # -- shell session.
473
+ # --
474
+ app_id = KeyId.derive_app_instance_identifier( domain_name )
475
+ unique_id = KeyId.derive_universal_id( app_id, to_token() )
476
+ crumbs_db.use( unique_id )
477
+ crumbs_db.set( INTRA_KEY_CIPHERTEXT, intra_txt )
478
+ crumbs_db.set( SESSION_LOGIN_DATETIME, KeyNow.fetch() )
479
+
480
+ # --
481
+ # -- Switch the dominant application domain being used to
482
+ # -- the domain that has just logged in.
483
+ # --
484
+ use_application_domain( domain_name )
485
+
486
+ end
487
+
488
+
489
+ # Switch the application instance that the current shell session is using.
490
+ # Trigger this method either during the login use case or when the user
491
+ # issues an intent to use a different application instance.
492
+ #
493
+ # The machine configuration file at path {MACHINE_CONFIG_FILE} is changed
494
+ # in the following way
495
+ #
496
+ # - a {SESSION_APP_DOMAINS} section is added if one does not exist
497
+ # - the shell session ID key is added (or updated if it exists)
498
+ # - with a value corresponding to the app instance ID (on this machine)
499
+ #
500
+ # Subsequent use cases can now access the application ID by going first to
501
+ # the {SESSION_APP_DOMAINS} section, reading the ID of the app instance on
502
+ # this machine and then using that in turn to read the {APP_INSTANCE_ID_KEY}
503
+ # value.
504
+ #
505
+ # The {APP_INSTANCE_ID_KEY} value is the global ID of the app instance no
506
+ # matter which machine or shell is being used.
507
+ #
508
+ # @param domain_name [String]
509
+ # the string reference that points to the global application identifier
510
+ # no matter the machine being used.
511
+ def self.use_application_domain( domain_name )
512
+
513
+ KeyError.not_new( domain_name, self )
514
+
515
+ aim_id = KeyId.derive_app_instance_machine_id( domain_name )
516
+ sid_id = KeyId.derive_session_id( to_token() )
517
+
518
+ keypairs = KeyPair.new( MACHINE_CONFIG_FILE )
519
+ keypairs.use( SESSION_APP_DOMAINS )
520
+ keypairs.set( sid_id, aim_id )
521
+
522
+ end
523
+
524
+
525
+ # <b>Logout of the shell key session</b> by making the high entropy content
526
+ # encryption key <b>irretrievable for all intents and purposes</b> to anyone
527
+ # who does not possess the domain secret.
528
+ #
529
+ # The key logout action is deleting the ciphertext originally produced when
530
+ # the intra-sessionary (shell) key encrypted the content encryption key.
531
+ #
532
+ # <b>Why Isn't the Session Token Deleted?</b>
533
+ #
534
+ # The session token is left to <b>die by natural causes</b> so that we don't
535
+ # interfere with other domain interactions that may be in progress within
536
+ # this shell session.
537
+ #
538
+ # @param domain_name [String]
539
+ # the string reference that points to the application instance that we
540
+ # are logging out of from the shell on this machine.
541
+ def self.do_logout( domain_name )
542
+
543
+ # --> @todo - user should ONLY type in ops logout | without domain name
544
+ # --> @todo - user should ONLY type in ops logout | without domain name
545
+ # --> @todo - user should ONLY type in ops logout | without domain name
546
+ # --> @todo - user should ONLY type in ops logout | without domain name
547
+ # --> @todo - user should ONLY type in ops logout | without domain name
548
+
549
+
550
+ # --> ######################
551
+ # --> Login / Logout Time
552
+ # --> ######################
553
+ # -->
554
+ # --> During login you create a section heading same as the session ID
555
+ # --> You then put the intra-key ciphertext there (from locking power key)
556
+ # --> To check if a login has occurred we ensure this session's ID exists as a header in crumbs DB
557
+ # --> On logout we remove the session ID and all the subsection crumbs (intra key ciphertext)
558
+ # --> Logout makes it impossible to access the power key (now only by seret delivery and the inter key ciphertext)
559
+ # -->
560
+
561
+
562
+ # --
563
+ # -- Get the breadcrumbs trail.
564
+ # --
565
+ crumbs_db = get_crumbs_db_from_domain_name( domain_name )
566
+
567
+
568
+ # --
569
+ # -- Set the (ciphertext) breadcrumbs for re-acquiring the
570
+ # -- content encryption (power) key during (inside) this
571
+ # -- shell session.
572
+ # --
573
+ unique_id = KeyId.derive_universal_id( domain_name )
574
+ crumbs_db.use( unique_id )
575
+ crumbs_db.set( INTRA_KEY_CIPHERTEXT, intra_txt )
576
+ crumbs_db.set( SESSION_LOGOUT_DATETIME, KeyNow.fetch() )
577
+
578
+ end
579
+
580
+
581
+ # Has the user orchestrating this shell session logged in? Yes or no?
582
+ # If yes then they appear to have supplied the correct secret
583
+ #
584
+ # - in this shell session
585
+ # - on this machine and
586
+ # - for this application instance
587
+ #
588
+ # Use the crumbs found underneath the universal (session) ID within the
589
+ # main breadcrumbs file for this application instance.
590
+ #
591
+ # Note that the system does not rely on this value for its security, it
592
+ # exists only to give a pleasant error message.
593
+ #
594
+ # @return [Boolean]
595
+ # return true if a marker denoting that this shell session with this
596
+ # application instance on this machine has logged in. Subverting this
597
+ # return value only serves to evoke disgraceful degradation.
598
+ def self.is_logged_in?( domain_name )
599
+ ############## Write this code.
600
+ ############## Write this code.
601
+ ############## Write this code.
602
+ ############## Write this code.
603
+ ############## Write this code.
604
+ ############## Write this code.
605
+ ############## Write this code.
606
+ return false unless File.exists?( frontend_keystore_file() )
607
+
608
+ crumbs_db = KeyPair.new( frontend_keystore_file() )
609
+ crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
610
+ return false unless crumbs_db.contains?( LOGGED_IN_APP_SESSION_ID )
611
+
612
+ recorded_id = crumbs_db.get( LOGGED_IN_APP_SESSION_ID )
613
+ return recorded_id.eql?( @uni_id )
614
+
615
+ end
616
+
617
+
618
+ # To read the content we first find the appropriate shell key and the
619
+ # appropriate database ciphertext, one decrypts the other to produce the index
620
+ # database crypt key and that decrypts the current database's ciphertext to
621
+ # reveal the database in plaintext.
622
+ #
623
+ # The plaintext is deserialized into a data structure and returned.
624
+ #
625
+ # <b>Steps Taken To Read the Content</b>
626
+ #
627
+ # Reading the content requires a rostra of actions namely
628
+ #
629
+ # - reading the path to the <b>keystore breadcrumbs file</b>
630
+ # - using the session token to derive the (unique to the) shell key
631
+ # - using the shell key and ciphertext to unlock the index key
632
+ # - reading the encrypted and encoded content, decoding and decrypting it
633
+ # - employing index key, ciphertext and random iv to reveal the content
634
+ #
635
+ # @return [String]
636
+ # decode, decrypt and hen return the plain text content that was written
637
+ # to a file by the {write_content} method.
638
+ def self.read_app_content()
639
+
640
+ # --
641
+ # -- Get the filepath to the breadcrumbs file using the trail in
642
+ # -- the global configuration left by {use_application_domain}.
643
+ # --
644
+ crumbs_db = get_crumbs_db_from_session_token()
645
+
646
+ # --
647
+ # -- Get the path to the file holding the ciphertext of the application
648
+ # -- database content locked by the content encryption key.
649
+ # --
650
+ crypt_filepath = content_ciphertxt_file_from_session_token()
651
+
652
+ # --
653
+ # -- Regenerate intra-session key from the session token.
654
+ # --
655
+ intra_key = KeyLocal.regenerate_shell_key( to_token() )
656
+
657
+ # --
658
+ # -- Decrypt and acquire the content enryption key that was created
659
+ # -- during the login use case and encrypted using the intra sessionary
660
+ # -- key.
661
+ # --
662
+ unique_id = KeyId.derive_universal_id( read_app_id(), to_token() )
663
+ crumbs_db.use( unique_id )
664
+ power_key = intra_key.do_decrypt_key( crumbs_db.get( INTRA_KEY_CIPHERTEXT ) )
665
+
666
+ # --
667
+ # -- Set the (ciphertext) breadcrumbs for re-acquiring the
668
+ # -- content encryption (power) key during (inside) this
669
+ # -- shell session.
670
+ # --
671
+ crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
672
+ random_iv = KeyIV.in_binary( crumbs_db.get( INDEX_DB_CRYPT_IV_KEY ) )
673
+
674
+ # --
675
+ # -- Get the full ciphertext file (warts and all) and then top and
676
+ # -- tail until just the valuable ciphertext is at hand. Decode then
677
+ # -- decrypt the ciphertext and instantiate a key database from it.
678
+ # --
679
+ crypt_txt = binary_from_read( crypt_filepath )
680
+
681
+ json_content = power_key.do_decrypt_text( random_iv, crypt_txt )
682
+
683
+ return KeyDb.from_json( json_content )
684
+
685
+ end
686
+
687
+
688
+ # This write content behaviour takes the parameter content, encyrpts and
689
+ # encodes it using the index key, which is itself derived from the shell
690
+ # key unlocking the intra session ciphertext. The crypted content is
691
+ # written to a file whose path is derviced by {content_ciphertxt_file_from_domain_name}.
692
+ #
693
+ # <b>Steps Taken To Write the Content</b>
694
+ #
695
+ # Writing the content requires a rostra of actions namely
696
+ #
697
+ # - deriving filepaths to both the breadcrumb and ciphertext files
698
+ # - creating a random iv and adding its base64 form to the breadcrumbs
699
+ # - using the session token to derive the (unique to the) shell key
700
+ # - using the shell key and (intra) ciphertext to acquire the index key
701
+ # - using the index key and random iv to encrypt and encode the content
702
+ # - writing the resulting ciphertext to a file at the designated path
703
+ #
704
+ # @param content_header [String]
705
+ # the string that will top the ciphertext content when it is written
706
+ #
707
+ # @param app_database [KeyDb]
708
+ # this key database class will be streamed using its {Hash.to_json}
709
+ # method and the resulting content will be encrypted and written to
710
+ # the file at path {content_ciphertxt_file_from_session_token}.
711
+ #
712
+ # This method's mirror is {read_app_content}.
713
+ def self.write_app_content( content_header, app_database )
714
+
715
+ # --
716
+ # -- Get the filepath to the breadcrumbs file using the trail in
717
+ # -- the global configuration left by {use_application_domain}.
718
+ # --
719
+ crumbs_db = get_crumbs_db_from_session_token()
720
+
721
+ # --
722
+ # -- Get the path to the file holding the ciphertext of the application
723
+ # -- database content locked by the content encryption key.
724
+ # --
725
+ crypt_filepath = content_ciphertxt_file_from_session_token()
726
+
727
+ # --
728
+ # -- Regenerate intra-session key from the session token.
729
+ # --
730
+ intra_key = KeyLocal.regenerate_shell_key( to_token() )
731
+
732
+ # --
733
+ # -- Decrypt and acquire the content enryption key that was created
734
+ # -- during the login use case and encrypted using the intra sessionary
735
+ # -- key.
736
+ # --
737
+ unique_id = KeyId.derive_universal_id( read_app_id(), to_token() )
738
+ crumbs_db.use( unique_id )
739
+ power_key = intra_key.do_decrypt_key( crumbs_db.get( INTRA_KEY_CIPHERTEXT ) )
740
+
741
+ # --
742
+ # -- Create a new random initialization vector (iv) to use when
743
+ # -- encrypting the incoming database content before writing it
744
+ # -- out to the file at the crypt filepath.
745
+ # --
746
+ iv_base64_chars = KeyIV.new().for_storage()
747
+ crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
748
+ crumbs_db.set( INDEX_DB_CRYPT_IV_KEY, iv_base64_chars )
749
+ random_iv = KeyIV.in_binary( iv_base64_chars )
750
+
751
+ # --
752
+ # -- Now we use the content encryption (power) key and the random initialization
753
+ # -- vector (iv) to first encrypt the incoming content and then to Base64 encode
754
+ # -- the result. This is then written into the crypt filepath derived earlier.
755
+ # --
756
+ binary_ciphertext = power_key.do_encrypt_text( random_iv, app_database.to_json )
757
+ binary_to_write( crypt_filepath, content_header, binary_ciphertext )
758
+
759
+ end
760
+
761
+
762
+ # Register the URL to the <b>frontend keystore</b> that is tied to
763
+ # this application instance on this workstation (and user). The default
764
+ # keystore sits on an accessible filesystem that is preferably a
765
+ # removable drive (like a USB key or phone) which allows the keys to
766
+ # your secrets to travel with you in your pocket.
767
+ #
768
+ # <b>Changing the Keystore Url</b>
769
+ #
770
+ # If the keystore url has already been configured this method will overwrite
771
+ # (thereby updating) it.
772
+ #
773
+ # <b>Changing the Keystore Url</b>
774
+ #
775
+ # The keystore directives in the global configuration file looks like this.
776
+ #
777
+ # [keystore.ids]
778
+ # dxEy-v2w3-x7y8 = /media/usb_key/family.creds
779
+ # 47S3-Nv0w-8SYf = /media/usb_key/friend.creds
780
+ # 3Dds-8Tts-Jy2G = /media/usb_key/office.creds
781
+ #
782
+ # <b>Which Use Case Sets the Keystore Url?</b>
783
+ #
784
+ # The keystore url must be provided <b>the very first time</b> init
785
+ # is called for an app instance on a machine. If the configuration
786
+ # is wiped, the next initialize use case must again provide it.
787
+ #
788
+ # <b>How to Add (Extend) Storage Services</b>
789
+ #
790
+ # We could use Redis, PostgreSQL, even a Rest API to provide storage
791
+ # services. To extend it - make a keystore ID boss its own section and
792
+ # then add keypairs like
793
+ #
794
+ # - the keystore URL
795
+ # - the keystore Type (or interface class)
796
+ # - keystore create destroy markers
797
+ #
798
+ # @param keystore_url [String]
799
+ # The keystore url points to where the key metadata protecting
800
+ # this application instance lives. The simplest keystores are
801
+ # based on files and for them this url is just a folder path.
802
+ #
803
+ # @raise [KeyError]
804
+ #
805
+ # The keystore URL cannot be <b>NEW</b>. The <b>NEW acronym</b> asserts
806
+ # that the attribute is
807
+ #
808
+ # - neither <b>N</b>il
809
+ # - nor <b>E</b>mpty
810
+ # - nor <b>W</b>hitespace only
811
+ #
812
+ def register_keystore keystore_url
813
+ KeyError.not_new( keystore_url, self )
814
+ @keymap.write( @aim_id, KEYSTORE_IDENTIFIER_KEY, keystore_url )
815
+ end
816
+
817
+
818
+ # Generate a new set of envelope breadcrumbs, derive the new envelope
819
+ # filepath, then <b>encrypt</b> the raw envelope content, and write the
820
+ # resulting ciphertext out into the new file.
821
+ #
822
+ # The important parameters in play are the
823
+ #
824
+ # - session token used to find the storage folder
825
+ # - random envelope external ID used to name the ciphertext file
826
+ # - generated random key for encrypting and decrypting the content
827
+ # - generated random initialization vector (IV) for crypting
828
+ # - name of the file in which the locked content is placed
829
+ # - header and footer content that tops and tails the ciphertext
830
+ #
831
+ # @param crumbs_map [Hash]
832
+ #
833
+ # nothing is read from this crumbs map but 3 things are written to
834
+ # it with these corresponding key names
835
+ #
836
+ # - random content external ID {CONTENT_EXTERNAL_ID}
837
+ # - high entropy crypt key {CONTENT_ENCRYPT_KEY}
838
+ # - and initialization vector {CONTENT_RANDOM_IV}
839
+ #
840
+ # @param content_body [String]
841
+ #
842
+ # this is the envelope's latest and greatest content that will
843
+ # be encrypted, encoded, topped, tailed and then pushed out to
844
+ # the domain's storage folder.
845
+ #
846
+ # @param content_header [String]
847
+ #
848
+ # the string that will top the ciphertext content when it is written
849
+ #
850
+ def self.content_lock( crumbs_map, content_body, content_header )
851
+
852
+ # --
853
+ # -- Create the external content ID and place
854
+ # -- it within the crumbs map.
855
+ # --
856
+ content_exid = get_random_reference()
857
+ crumbs_map[ CONTENT_EXTERNAL_ID ] = content_exid
858
+
859
+ # --
860
+ # -- Create a random initialization vector (iv)
861
+ # -- for AES encryption and store it within the
862
+ # -- breadcrumbs map.
863
+ # --
864
+ iv_base64 = KeyIV.new().for_storage()
865
+ random_iv = KeyIV.in_binary( iv_base64 )
866
+ crumbs_map[ CONTENT_RANDOM_IV ] = iv_base64
867
+
868
+ # --
869
+ # -- Create a new high entropy random key for
870
+ # -- locking the content with AES. Place the key
871
+ # -- within the breadcrumbs map.
872
+ # --
873
+ crypt_key = Key.from_random()
874
+ crumbs_map[ CONTENT_ENCRYPT_KEY ] = crypt_key.to_char64()
875
+
876
+ # --
877
+ # -- Now use AES to lock the content body and write
878
+ # -- the encoded ciphertext out to a file that is
879
+ # -- topped with the parameter content header.
880
+ # --
881
+ binary_ctext = crypt_key.do_encrypt_text( random_iv, content_body )
882
+ content_path = content_filepath( content_exid )
883
+ binary_to_write( content_path, content_header, binary_ctext )
884
+
885
+ end
886
+
887
+
888
+ # Use the content's external id expected in the breadcrumbs together with
889
+ # the session token to derive the content's filepath and then unlock and
890
+ # the content as a {KeyDb} structure.
891
+ #
892
+ # Unlocking the content means reading it, decoding and then decrypting it using
893
+ # the initialization vector (iv) and decryption key whose values are expected
894
+ # within the breadcrumbs map.
895
+ #
896
+ # @param crumbs_map [Hash]
897
+ #
898
+ # the three (3) data points expected within the breadcrumbs map are the
899
+ #
900
+ # - content's external ID {CONTENT_EXTERNAL_ID}
901
+ # - AES encryption key {CONTENT_ENCRYPT_KEY}
902
+ # - initialization vector {CONTENT_RANDOM_IV}
903
+ #
904
+ def self.content_unlock( crumbs_map )
905
+
906
+ # --
907
+ # -- Get the external ID of the content then use
908
+ # -- that plus the session context to derive the
909
+ # -- content's ciphertext filepath.
910
+ # --
911
+ content_path = content_filepath( crumbs_map[ CONTENT_EXTERNAL_ID ] )
912
+
913
+ # --
914
+ # -- Read the binary ciphertext of the content
915
+ # -- from the file. Then decrypt it using the
916
+ # -- AES crypt key and intialization vector.
917
+ # --
918
+ crypt_txt = binary_from_read( content_path )
919
+ random_iv = KeyIV.in_binary( crumbs_map[ CONTENT_RANDOM_IV ] )
920
+ crypt_key = Key.from_char64( crumbs_map[ CONTENT_ENCRYPT_KEY ] )
921
+ json_text = crypt_key.do_decrypt_text( random_iv, crypt_txt )
922
+
923
+ # --
924
+ # -- The decrypted content is expected to be a tree data structure
925
+ # -- streamed into JSON before encryption.
926
+ # --
927
+ return KeyDb.from_json( json_text )
928
+
929
+ end
930
+
931
+
932
+ # This method returns the <b>content filepath</b> which (at its core)
933
+ # is an amalgam of the application's (domain) identifier and the content's
934
+ # external identifier (XID).
935
+ #
936
+ # The filename is prefixed by {CONTENT_FILE_PREFIX}.
937
+ #
938
+ # @param external_id [String]
939
+ #
940
+ # nothing is read from this crumbs map but 3 things are written to
941
+ # it with these corresponding key names
942
+ #
943
+ # - random content external ID {CONTENT_EXTERNAL_ID}
944
+ # - high entropy crypt key {CONTENT_ENCRYPT_KEY}
945
+ # - and initialization vector {CONTENT_RANDOM_IV}
946
+ def self.content_filepath( external_id )
947
+
948
+ app_identity = read_app_id()
949
+ store_folder = get_store_folder()
950
+ env_filename = "#{CONTENT_FILE_PREFIX}.#{external_id}.#{app_identity}.txt"
951
+ env_filepath = File.join( store_folder, env_filename )
952
+ return env_filepath
953
+
954
+ end
955
+
956
+
957
+ # If the <b>content dictionary is not nil</b> and contains a key named
958
+ # {CONTENT_EXTERNAL_ID} then we return true as we expect the content
959
+ # ciphertext and its corresponding file to exist.
960
+ #
961
+ # This method throws an exception if they key exists but there is no
962
+ # file at the expected location.
963
+ #
964
+ # @param crumbs_map [Hash]
965
+ #
966
+ # we test for the existence of the constant {CONTENT_EXTERNAL_ID}
967
+ # and if it exists we assert that the content filepath should also
968
+ # be present.
969
+ #
970
+ def self.content_exists?( crumbs_map )
971
+
972
+ return false if crumbs_map.nil?
973
+ return false unless crumbs_map.has_key?( CONTENT_EXTERNAL_ID )
974
+
975
+ external_id = crumbs_map[ CONTENT_EXTERNAL_ID ]
976
+ the_filepath = content_filepath( external_id )
977
+ error_string = "External ID #{external_id} found but no file at #{the_filepath}"
978
+ raise RuntimeException, error_string unless File.file?( the_filepath )
979
+
980
+ return true
981
+
982
+ end
983
+
984
+
985
+ # Construct the header for the ciphertext content files written out
986
+ # onto the filesystem.
987
+ #
988
+ # @param gem_version [String] the current version number of the calling gem
989
+ # @param gem_name [String] the current name of the calling gem
990
+ # @param gem_site [String] the current website of the calling gem
991
+ #
992
+ # @param the_domain_name [String]
993
+ #
994
+ # This method uses one of the two (2) ways to gain the application id.
995
+ #
996
+ # If not logged in callers will have the domain name and should pass it
997
+ # in so that this method can use {KeyId.derive_app_instance_identifier}
998
+ # to gain the application id.
999
+ #
1000
+ # If logged in then method {KeyApi.use_application_domain} will have
1001
+ # executed and the application ID will be written inside the
1002
+ # <b>machine configuration file</b> under the application instance on
1003
+ # machine id and referenced in turn from the {SESSION_APP_DOMAINS} map.
1004
+ #
1005
+ # In the above case post a NIL domain name and this method will now
1006
+ # turn to {KeyApi.read_app_id} for the application id.
1007
+ def self.format_header( gem_version, gem_name, gem_site, the_domain_name = nil )
1008
+
1009
+ application_id = KeyId.derive_app_instance_identifier(the_domain_name) unless the_domain_name.nil?
1010
+ application_id = read_app_id() if the_domain_name.nil?
1011
+ universal_id = KeyId.derive_universal_id( application_id, to_token() )
1012
+
1013
+ line1 = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
1014
+ line2 = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
1015
+ line3 = "#{gem_name} ciphertext block\n"
1016
+ line4 = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
1017
+ line5 = "App Ref Num := #{application_id}\n" # application domain reference
1018
+ line6 = "Access Time := #{KeyNow.grab()}\n" # timestamp of the last write
1019
+ line7 = "App Version := #{gem_version}\n" # this application semantic version
1020
+ line8 = "Website Url := #{gem_site}\n" # app website or github url
1021
+ line9 = "Session Ref := #{universal_id}\n" # application domain reference
1022
+
1023
+ return line1 + line2 + line3 + line4 + line5 + line6 + line7 + line8 + line9
1024
+
1025
+ end
1026
+
1027
+
1028
+ private
1029
+
1030
+
1031
+
1032
+ MACHINE_CONFIG_FILE = File.join( Dir.home, ".config/openkey/openkey.app.config.ini" )
1033
+
1034
+ SESSION_APP_DOMAINS = "session.app.domains"
1035
+ TOKEN_VARIABLE_NAME = "OPEN_SESSION_TOKEN"
1036
+ TOKEN_VARIABLE_SIZE = 150
1037
+
1038
+ SESSION_IDENTIFIER_KEY = "session.identifiers"
1039
+ KEYSTORE_IDENTIFIER_KEY = "keystore.url.id"
1040
+ APP_INSTANCE_ID_KEY = "app.instance.id"
1041
+ AIM_IDENTITY_REF_KEY = "aim.identity.ref"
1042
+ LOGIN_TIMESTAMP_KEY = "login.timestamp"
1043
+ LOGOUT_TIMESTAMP_KEY = "logout.timestamp"
1044
+ MACHINE_CONFIGURATION = "machine.configuration"
1045
+ APP_INITIALIZE_TIME = "initialize.time"
1046
+
1047
+ APP_INSTANCE_SETUP_TIME = "app.instance.setup.time"
1048
+
1049
+ APP_KEY_DB_NAME_PREFIX = "openkey.breadcrumbs"
1050
+ FILE_CIPHERTEXT_PREFIX = "openkey.cipher.file"
1051
+ OK_BASE_FOLDER_PREFIX = "openkey.store"
1052
+ OK_BACKEND_CRYPT_PREFIX = "backend.crypt"
1053
+
1054
+ APP_KEY_DB_DIRECTIVES = "key.db.directives"
1055
+ APP_KEY_DB_CREATE_TIME_KEY = "initialize.time"
1056
+ APP_KEY_DB_BREAD_CRUMBS = "openkey.bread.crumbs"
1057
+
1058
+ LOGGED_IN_APP_SESSION_ID = "logged.in.app.session.id"
1059
+ SESSION_LOGIN_DATETIME = "session.login.datetime"
1060
+ SESSION_LOGOUT_DATETIME = "session.logout.datetime"
1061
+
1062
+ INTER_KEY_CIPHERTEXT = "inter.key.ciphertext"
1063
+ INTRA_KEY_CIPHERTEXT = "intra.key.ciphertext"
1064
+ INDEX_DB_CRYPT_IV_KEY = "index.db.cipher.iv"
1065
+
1066
+ BLOCK_64_START_STRING = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab\n"
1067
+ BLOCK_64_END_STRING = "ba9876543210fedcba9876543210fedcba9876543210fedcba9876543210\n"
1068
+ BLOCK_64_DELIMITER = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
1069
+
1070
+ XID_SOURCE_APPROX_LEN = 11
1071
+
1072
+ CONTENT_FILE_PREFIX = "tree.db"
1073
+ CONTENT_EXTERNAL_ID = "content.xid"
1074
+ CONTENT_ENCRYPT_KEY = "content.key"
1075
+ CONTENT_RANDOM_IV = "content.iv"
1076
+
1077
+
1078
+ def self.binary_to_write( to_filepath, content_header, binary_ciphertext )
1079
+
1080
+ base64_ciphertext = Base64.encode64( binary_ciphertext )
1081
+
1082
+ content_to_write =
1083
+ content_header +
1084
+ BLOCK_64_DELIMITER +
1085
+ BLOCK_64_START_STRING +
1086
+ base64_ciphertext +
1087
+ BLOCK_64_END_STRING +
1088
+ BLOCK_64_DELIMITER
1089
+
1090
+ File.write( to_filepath, content_to_write )
1091
+
1092
+ end
1093
+
1094
+
1095
+ def self.binary_from_read( from_filepath )
1096
+
1097
+ file_text = File.read( from_filepath )
1098
+ core_data = file_text.in_between( BLOCK_64_START_STRING, BLOCK_64_END_STRING ).strip
1099
+ return Base64.decode64( core_data )
1100
+
1101
+ end
1102
+
1103
+
1104
+ def self.get_random_reference
1105
+
1106
+ # Do not forget that you can pass this through
1107
+ # the derive identifier method if uniformity is
1108
+ # what you seek.
1109
+ #
1110
+ # [ KeyId.derive_identifier( reference ) ]
1111
+ #
1112
+ random_ref = SecureRandom.urlsafe_base64( XID_SOURCE_APPROX_LEN ).delete("-_").downcase
1113
+ return random_ref[ 0 .. ( XID_SOURCE_APPROX_LEN - 1 ) ]
1114
+
1115
+ end
1116
+
1117
+
1118
+ def self.get_virgin_content( domain_name )
1119
+
1120
+ KeyError.not_new( domain_name, self )
1121
+ app_id = KeyId.derive_app_instance_identifier( domain_name )
1122
+
1123
+ initial_db = KeyDb.new()
1124
+ initial_db.store( "Database Birthday", KeyNow.fetch() )
1125
+ initial_db.store( "App Domain Name", domain_name )
1126
+ initial_db.store( "Application Id", app_id )
1127
+ return initial_db.to_json
1128
+
1129
+ end
1130
+
1131
+
1132
+ # This method depends on {use_application_domain} which sets
1133
+ # the application ID against the session identity so only call
1134
+ # it if we are in a logged in state.
1135
+ #
1136
+ # NOTE this will NOT be set until the session is logged in so
1137
+ # the call fails before that. For this reason do not call this
1138
+ # method from outside this class. If the domain name is
1139
+ # available use {KeyId.derive_app_instance_identifier} instead.
1140
+ def self.read_app_id()
1141
+
1142
+ aim_id = read_aim_id()
1143
+ keypairs = KeyPair.new( MACHINE_CONFIG_FILE )
1144
+ keypairs.use( aim_id )
1145
+ return keypairs.get( APP_INSTANCE_ID_KEY )
1146
+
1147
+ end
1148
+
1149
+
1150
+ def self.read_aim_id()
1151
+
1152
+ session_identifier = KeyId.derive_session_id( to_token() )
1153
+
1154
+ keypairs = KeyPair.new( MACHINE_CONFIG_FILE )
1155
+ keypairs.use( SESSION_APP_DOMAINS )
1156
+ return keypairs.get( session_identifier )
1157
+
1158
+ end
1159
+
1160
+
1161
+ def self.get_crumbs_db_from_domain_name( domain_name )
1162
+
1163
+ KeyError.not_new( domain_name, self )
1164
+ keystore_file = get_keystore_file_from_domain_name( domain_name )
1165
+ crumbs_db = KeyPair.new( keystore_file )
1166
+ crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
1167
+ return crumbs_db
1168
+
1169
+ end
1170
+
1171
+
1172
+ def self.get_crumbs_db_from_session_token()
1173
+
1174
+ keystore_file = get_keystore_file_from_session_token()
1175
+ crumbs_db = KeyPair.new( keystore_file )
1176
+ crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
1177
+ return crumbs_db
1178
+
1179
+ end
1180
+
1181
+
1182
+ def self.get_store_folder()
1183
+
1184
+ aim_id = read_aim_id()
1185
+ app_id = read_app_id()
1186
+ return get_app_keystore_folder( aim_id, app_id )
1187
+
1188
+ end
1189
+
1190
+
1191
+ def self.get_app_keystore_folder( aim_id, app_id )
1192
+
1193
+ keypairs = KeyPair.new( MACHINE_CONFIG_FILE )
1194
+ keypairs.use( aim_id )
1195
+ keystore_url = keypairs.get( KEYSTORE_IDENTIFIER_KEY )
1196
+ basedir_name = "#{OK_BASE_FOLDER_PREFIX}.#{app_id}"
1197
+ return File.join( keystore_url, basedir_name )
1198
+
1199
+ end
1200
+
1201
+
1202
+ def self.get_keystore_file_from_domain_name( domain_name )
1203
+
1204
+ aim_id = KeyId.derive_app_instance_machine_id( domain_name )
1205
+ app_id = KeyId.derive_app_instance_identifier( domain_name )
1206
+
1207
+ app_key_db_file = "#{APP_KEY_DB_NAME_PREFIX}.#{app_id}.ini"
1208
+ return File.join( get_app_keystore_folder( aim_id, app_id ), app_key_db_file )
1209
+
1210
+ end
1211
+
1212
+
1213
+ def self.get_keystore_file_from_session_token()
1214
+
1215
+ aim_id = read_aim_id()
1216
+ app_id = read_app_id()
1217
+
1218
+ app_key_db_file = "#{APP_KEY_DB_NAME_PREFIX}.#{app_id}.ini"
1219
+ return File.join( get_app_keystore_folder( aim_id, app_id ), app_key_db_file )
1220
+
1221
+ end
1222
+
1223
+
1224
+ def self.content_ciphertxt_file_from_domain_name( domain_name )
1225
+
1226
+ aim_id = KeyId.derive_app_instance_machine_id( domain_name )
1227
+ app_id = KeyId.derive_app_instance_identifier( domain_name )
1228
+
1229
+ appdb_cipher_file = "#{FILE_CIPHERTEXT_PREFIX}.#{app_id}.txt"
1230
+ return File.join( get_app_keystore_folder( aim_id, app_id ), appdb_cipher_file )
1231
+
1232
+ end
1233
+
1234
+
1235
+ def self.content_ciphertxt_file_from_session_token()
1236
+
1237
+ aim_id = read_aim_id()
1238
+ app_id = read_app_id()
1239
+
1240
+ appdb_cipher_file = "#{FILE_CIPHERTEXT_PREFIX}.#{app_id}.txt"
1241
+ return File.join( get_app_keystore_folder( aim_id, app_id ), appdb_cipher_file )
1242
+
1243
+ end
1244
+
1245
+
1246
+ def self.to_token()
1247
+
1248
+ raw_env_var_value = ENV[TOKEN_VARIABLE_NAME]
1249
+ raise_token_error( TOKEN_VARIABLE_NAME, "not present") unless raw_env_var_value
1250
+
1251
+ env_var_value = raw_env_var_value.strip
1252
+ raise_token_error( TOKEN_VARIABLE_NAME, "consists only of whitespace") if raw_env_var_value.empty?
1253
+
1254
+ size_msg = "length should contain exactly #{TOKEN_VARIABLE_SIZE} characters"
1255
+ raise_token_error( TOKEN_VARIABLE_NAME, size_msg ) unless env_var_value.length == TOKEN_VARIABLE_SIZE
1256
+
1257
+ return env_var_value
1258
+
1259
+ end
1260
+
1261
+
1262
+ def raise_token_error env_var_name, message
1263
+
1264
+ puts ""
1265
+ puts "#{TOKEN_VARIABLE_NAME} environment variable #{message}."
1266
+ puts "To instantiate it you can use the below command."
1267
+ puts ""
1268
+ puts "$ export OPEN_SESSION_TOKEN=`ops token`"
1269
+ puts ""
1270
+ puts "ps => those are backticks around `ops token` (not apostrophes)."
1271
+ puts ""
1272
+
1273
+ raise RuntimeError, "#{TOKEN_VARIABLE_NAME} environment variable #{message}."
1274
+
1275
+ end
1276
+
1277
+
1278
+ end
1279
+
1280
+
1281
+ end