safedb 0.01.0001

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