safedb 0.01.0001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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