safedb 0.3.1011 → 0.4.1002

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +56 -19
  3. data/README.md +15 -15
  4. data/Rakefile +7 -0
  5. data/bin/safe +2 -2
  6. data/lib/{interprete.rb → cli.rb} +168 -121
  7. data/lib/controller/admin/README.md +47 -0
  8. data/lib/controller/admin/access.rb +47 -0
  9. data/lib/controller/admin/checkin.rb +83 -0
  10. data/lib/controller/admin/checkout.rb +57 -0
  11. data/lib/controller/admin/diff.rb +75 -0
  12. data/lib/{usecase → controller/admin}/export.rb +15 -14
  13. data/lib/controller/admin/goto.rb +52 -0
  14. data/lib/controller/admin/import.rb +54 -0
  15. data/lib/controller/admin/init.rb +113 -0
  16. data/lib/controller/admin/login.rb +88 -0
  17. data/lib/{usecase → controller/admin}/logout.rb +0 -0
  18. data/lib/controller/admin/open.rb +39 -0
  19. data/lib/{usecase → controller/admin}/token.rb +2 -2
  20. data/lib/controller/admin/tree.md +54 -0
  21. data/lib/{usecase → controller/admin}/use.rb +0 -0
  22. data/lib/controller/admin/view.rb +61 -0
  23. data/lib/{usecase → controller/api}/docker/README.md +0 -0
  24. data/lib/{usecase → controller/api}/docker/docker.rb +1 -1
  25. data/lib/{usecase → controller/api}/jenkins/README.md +0 -0
  26. data/lib/{usecase → controller/api}/jenkins/jenkins.rb +1 -1
  27. data/lib/{usecase → controller/api}/terraform/README.md +1 -1
  28. data/lib/{usecase → controller/api}/terraform/terraform.rb +1 -1
  29. data/lib/{usecase → controller/api}/vpn/README.md +1 -1
  30. data/lib/{usecase → controller/api}/vpn/vpn.ini +0 -0
  31. data/lib/{usecase → controller/api}/vpn/vpn.rb +0 -0
  32. data/lib/{usecase → controller}/config/README.md +0 -0
  33. data/lib/{usecase → controller}/edit/README.md +0 -0
  34. data/lib/controller/edit/editverse.rb +48 -0
  35. data/lib/controller/edit/put.rb +35 -0
  36. data/lib/controller/edit/remove.rb +29 -0
  37. data/lib/{usecase/update/README.md → controller/edit/rename.md} +0 -0
  38. data/lib/{usecase → controller}/files/README.md +1 -1
  39. data/lib/controller/files/read.rb +36 -0
  40. data/lib/{usecase/files/eject.rb → controller/files/write.rb} +15 -20
  41. data/lib/{usecase → controller}/id.rb +0 -0
  42. data/lib/controller/query/print.rb +26 -0
  43. data/lib/controller/query/queryverse.rb +39 -0
  44. data/lib/controller/query/show.rb +50 -0
  45. data/lib/{session/require.gem.rb → controller/requirer.rb} +13 -9
  46. data/lib/{usecase → controller}/set.rb +4 -4
  47. data/lib/controller/usecase.rb +244 -0
  48. data/lib/{usecase → controller}/verse.rb +0 -0
  49. data/lib/{usecase → controller}/visit/README.md +0 -0
  50. data/lib/{usecase → controller}/visit/visit.rb +0 -0
  51. data/lib/factbase/facts.safedb.net.ini +7 -7
  52. data/lib/{keytools/key.docs.rb → model/README.md} +102 -66
  53. data/lib/model/book.rb +484 -0
  54. data/lib/model/branch.rb +48 -0
  55. data/lib/model/checkin.feature +33 -0
  56. data/lib/{configs/README.md → model/configs.md} +4 -4
  57. data/lib/model/content.rb +214 -0
  58. data/lib/model/indices.rb +132 -0
  59. data/lib/model/safe_tree.rb +51 -0
  60. data/lib/model/state.inspect.rb +221 -0
  61. data/lib/model/state.migrate.rb +334 -0
  62. data/lib/model/text_chunk.rb +68 -0
  63. data/lib/{extension → utils/extend}/array.rb +0 -0
  64. data/lib/{extension → utils/extend}/dir.rb +0 -0
  65. data/lib/{extension → utils/extend}/file.rb +0 -0
  66. data/lib/utils/extend/hash.rb +76 -0
  67. data/lib/{extension → utils/extend}/string.rb +6 -6
  68. data/lib/{session/fact.finder.rb → utils/facts/fact.rb} +0 -0
  69. data/lib/utils/identity/identifier.rb +356 -0
  70. data/lib/{keytools/key.ident.rb → utils/identity/machine.id.rb} +67 -4
  71. data/lib/utils/inspect/inspector.rb +81 -0
  72. data/lib/{keytools/kdf.bcrypt.rb → utils/kdfs/bcrypt.rb} +0 -0
  73. data/lib/{keytools → utils/kdfs}/kdf.api.rb +16 -16
  74. data/lib/{keytools/key.local.rb → utils/kdfs/kdfs.rb} +40 -40
  75. data/lib/{keytools/kdf.pbkdf2.rb → utils/kdfs/pbkdf2.rb} +0 -0
  76. data/lib/{keytools/kdf.scrypt.rb → utils/kdfs/scrypt.rb} +0 -0
  77. data/lib/{keytools → utils}/key.error.rb +2 -2
  78. data/lib/{keytools → utils}/key.pass.rb +2 -2
  79. data/lib/{keytools → utils/keys}/key.64.rb +0 -0
  80. data/lib/{keytools → utils/keys}/key.rb +6 -2
  81. data/lib/{keytools/key.iv.rb → utils/keys/random.iv.rb} +0 -0
  82. data/lib/{logging/gem.logging.rb → utils/logs/logger.rb} +6 -5
  83. data/lib/{keytools/key.pair.rb → utils/store/datamap.rb} +48 -30
  84. data/lib/{keytools/key.db.rb → utils/store/datastore.rb} +38 -104
  85. data/lib/utils/store/merge-boys-school.json +40 -0
  86. data/lib/utils/store/merge-girls-school.json +48 -0
  87. data/lib/utils/store/merge-merged-data.json +56 -0
  88. data/lib/utils/store/struct.rb +75 -0
  89. data/lib/utils/store/test-commands.sh +24 -0
  90. data/lib/{keytools/key.now.rb → utils/time/timestamp.rb} +32 -21
  91. data/lib/version.rb +1 -1
  92. metadata +86 -73
  93. data/lib/extension/hash.rb +0 -33
  94. data/lib/keytools/key.algo.rb +0 -109
  95. data/lib/keytools/key.api.rb +0 -1326
  96. data/lib/keytools/key.id.rb +0 -322
  97. data/lib/modules/cryptology/amalgam.rb +0 -70
  98. data/lib/modules/cryptology/engineer.rb +0 -99
  99. data/lib/modules/mappers/dictionary.rb +0 -288
  100. data/lib/session/time.stamp.rb +0 -340
  101. data/lib/session/user.home.rb +0 -49
  102. data/lib/usecase/cmd.rb +0 -471
  103. data/lib/usecase/edit/delete.rb +0 -46
  104. data/lib/usecase/files/file_me.rb +0 -78
  105. data/lib/usecase/files/read.rb +0 -169
  106. data/lib/usecase/files/write.rb +0 -89
  107. data/lib/usecase/goto.rb +0 -57
  108. data/lib/usecase/import.rb +0 -157
  109. data/lib/usecase/init.rb +0 -61
  110. data/lib/usecase/login.rb +0 -72
  111. data/lib/usecase/open.rb +0 -71
  112. data/lib/usecase/print.rb +0 -40
  113. data/lib/usecase/put.rb +0 -81
  114. data/lib/usecase/show.rb +0 -138
  115. data/lib/usecase/update/rename.rb +0 -180
  116. data/lib/usecase/view.rb +0 -71
@@ -1,33 +0,0 @@
1
- #!/usr/bin/ruby
2
-
3
- # Reopen the core ruby Hash class and add the below methods to it.
4
- class Hash
5
-
6
- # This method adds (logging its own contents) behaviour to
7
- # the standard library {Hash} class.
8
- #
9
- # @note This behaviour does not consider that SECRETS may be inside
10
- # the key value maps - it logs itself without a care in the world.
11
- # This functionality must be included if this behaviourr is used by
12
- # any cryptography classes.
13
- #
14
- # The <tt>DEBUG</tt> log level is used for logging. To change this
15
- # create a new parameterized method.
16
- def log_contents
17
-
18
- log.debug(x) { "# --- ----------------------------------------------" }
19
- log.debug(x) { "# --- Map has [#{self.length}] key/value pairs." }
20
- log.debug(x) { "# --- ----------------------------------------------" }
21
-
22
- self.each do |the_key, the_value|
23
-
24
- padded_key = sprintf '%-33s', the_key
25
- log.debug(x) { "# --- #{padded_key} => #{the_value}" }
26
-
27
- end
28
-
29
- log.debug(x) { "# --- ----------------------------------------------" }
30
-
31
- end
32
-
33
- end
@@ -1,109 +0,0 @@
1
- #!/usr/bin/ruby
2
- # coding: utf-8
3
-
4
- module SafeDb
5
-
6
- # Algorithms that are quality catalysts in the derivation and entropy spread
7
- # of keys, identifiers and base64 character numbers.
8
- class KeyAlgo
9
-
10
-
11
- # Cherry pick a given number of characters from the character pool
12
- # so that a good spread is achieved. This picker is the anti-pattern
13
- # of just axing the first 5 characters from a 100 character string
14
- # essentially wasting over 90% of the available entropy.
15
- #
16
- # This is the <b>algorithem to cherry pick</b> a spread of characters
17
- # from the pool in the second parameter.
18
- #
19
- # - if the character pool length is a multiple of num_chars all is good otherwise
20
- # - constrict to the <b>highest multiple of the pick size below</b> the pool length
21
- # - divide that number by num_chars to get the first offset and character spacing
22
- # - if spacing is 3, the first character is the 3rd, the second the 6th and so on
23
- # - then return the cherry picked characters
24
- #
25
- # @param pick_size [FixNum] the number of characters to cherry pick
26
- # @param char_pool [String] a pool of characters to cherry pick from
27
- # @return [String]
28
- # a string whose length is the one indicated by the first parameter
29
- # and whose characters contain a predictable, repeatable spread from
30
- # the character pool parameter
31
- def self.cherry_picker( pick_size, char_pool )
32
-
33
- hmb_limit = highest_multiple_below( pick_size, char_pool.length )
34
- jump_size = hmb_limit / pick_size
35
- read_point = jump_size
36
- picked_chars = ""
37
- loop do
38
- picked_chars += char_pool[ read_point - 1 ]
39
- read_point += jump_size
40
- break if read_point > hmb_limit
41
- end
42
-
43
- err_msg = "Expected cherry pick size to be #{pick_size} but it was #{picked_chars.length}."
44
- raise RuntimeError, err_msg unless picked_chars.length == pick_size
45
-
46
- return picked_chars
47
-
48
- end
49
-
50
-
51
- # Affectionately known as <b>a hmb</b>, this method returns the
52
- # <b>highest multiple</b> of the first parameter that is below
53
- # <b>(either less than or equal to)</b> the second parameter.
54
- #
55
- # - -------- - ------- - ----------------- -
56
- # | Small | Big | Highest Multiple |
57
- # | Number | Number | Below Big Num |
58
- # | -------- - ------- - ----------------- |
59
- # | 5 | 25 | 25 |
60
- # | 3 | 20 | 18 |
61
- # | 8 | 63 | 56 |
62
- # | 1 | 1 | 1 |
63
- # | 26 | 28 | 26 |
64
- # | 1 | 7 | 7 |
65
- # | 16 | 16 | 16 |
66
- # | -------- - ------- - ----------------- |
67
- # | 10 | 8 | ERROR |
68
- # | -4 | 17 | ERROR |
69
- # | 4 | -17 | ERROR |
70
- # | 0 | 32 | ERROR |
71
- # | 29 | 0 | ERROR |
72
- # | -4 | 0 | ERROR |
73
- # | -------- - ------- - ----------------- |
74
- # - -------- - ------- - ----------------- -
75
- #
76
- # Zeroes and negative numbers cannot be entertained, nor can the
77
- # small number be larger than the big one.
78
- #
79
- # @param small_num [FixNum]
80
- # the highest multiple of this number below the one in the
81
- # next parameter is what will be returned.
82
- #
83
- # @param big_num [FixNum]
84
- # returns either this number or the nearest below it that is
85
- # a multiple of the number in the first parameter.
86
- #
87
- # @raise [ArgumentError]
88
- # if the first parameter is greater than the second
89
- # if either or both parameters are zero or negative
90
- def self.highest_multiple_below small_num, big_num
91
-
92
- arg_issue = (small_num > big_num) || small_num < 1 || big_num < 1
93
- err_msg = "Invalid args #{small_num} and #{big_num} to HMB function."
94
- raise ArgumentError, err_msg if arg_issue
95
-
96
- for index in 0 .. ( big_num - 1 )
97
- invex = big_num - index # an [invex] is an inverted index
98
- return invex if invex % small_num == 0
99
- end
100
-
101
- raise ArgumentError, "Could not find a multiple of #{small_num} lower than #{big_num}"
102
-
103
- end
104
-
105
-
106
- end
107
-
108
-
109
- end
@@ -1,1326 +0,0 @@
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
- # @return [String]
733
- # decode, decrypt and hen return the plain text content that was written
734
- # to a file by the {write_content} method.
735
- def self.read_master_db()
736
-
737
- # --
738
- # -- Get the filepath to the breadcrumbs file using the trail in
739
- # -- the global configuration left by {use_application_domain}.
740
- # --
741
- crumbs_db = get_crumbs_db_from_session_token()
742
-
743
- # --
744
- # -- Get the path to the file holding the ciphertext of the application
745
- # -- database content locked by the content encryption key.
746
- # --
747
- crypt_filepath = content_ciphertxt_file_from_session_token()
748
-
749
- # --
750
- # -- Regenerate intra-session key from the session token.
751
- # --
752
- intra_key = KeyLocal.regenerate_shell_key( to_token() )
753
-
754
- # --
755
- # -- Decrypt and acquire the content enryption key that was created
756
- # -- during the login use case and encrypted using the intra sessionary
757
- # -- key.
758
- # --
759
- unique_id = KeyId.derive_universal_id( read_app_id(), to_token() )
760
- crumbs_db.use( unique_id )
761
- power_key = intra_key.do_decrypt_key( crumbs_db.get( INTRA_KEY_CIPHERTEXT ) )
762
-
763
- # --
764
- # -- Set the (ciphertext) breadcrumbs for re-acquiring the
765
- # -- content encryption (power) key during (inside) this
766
- # -- shell session.
767
- # --
768
- crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
769
- random_iv = KeyIV.in_binary( crumbs_db.get( INDEX_DB_CRYPT_IV_KEY ) )
770
-
771
- # --
772
- # -- Get the full ciphertext file (warts and all) and then top and
773
- # -- tail until just the valuable ciphertext is at hand. Decode then
774
- # -- decrypt the ciphertext and instantiate a key database from the
775
- # -- resulting JSON string.
776
- # --
777
- crypt_txt = binary_from_read( crypt_filepath )
778
- json_content = power_key.do_decrypt_text( random_iv, crypt_txt )
779
-
780
- return KeyDb.from_json( json_content )
781
-
782
- end
783
-
784
-
785
- # This write content behaviour takes the parameter content, encyrpts and
786
- # encodes it using the index key, which is itself derived from the shell
787
- # key unlocking the intra session ciphertext. The crypted content is
788
- # written to a file whose path is derviced by {content_ciphertxt_file_from_domain_name}.
789
- #
790
- # <b>Steps Taken To Write the Content</b>
791
- #
792
- # Writing the content requires a rostra of actions namely
793
- #
794
- # - deriving filepaths to both the breadcrumb and ciphertext files
795
- # - creating a random iv and adding its base64 form to the breadcrumbs
796
- # - using the session token to derive the (unique to the) shell key
797
- # - using the shell key and (intra) ciphertext to acquire the index key
798
- # - using the index key and random iv to encrypt and encode the content
799
- # - writing the resulting ciphertext to a file at the designated path
800
- #
801
- # @param content_header [String]
802
- # the string that will top the ciphertext content when it is written
803
- #
804
- # @param app_database [KeyDb]
805
- # this key database class will be streamed using its {Hash.to_json}
806
- # method and the resulting content will be encrypted and written to
807
- # the file at path {content_ciphertxt_file_from_session_token}.
808
- #
809
- # This method's mirror is {read_master_db}.
810
- def self.write_master_db( content_header, app_database )
811
-
812
- # --
813
- # -- Get the filepath to the breadcrumbs file using the trail in
814
- # -- the global configuration left by {use_application_domain}.
815
- # --
816
- crumbs_db = get_crumbs_db_from_session_token()
817
-
818
- # --
819
- # -- Get the path to the file holding the ciphertext of the application
820
- # -- database content locked by the content encryption key.
821
- # --
822
- crypt_filepath = content_ciphertxt_file_from_session_token()
823
-
824
- # --
825
- # -- Regenerate intra-session key from the session token.
826
- # --
827
- intra_key = KeyLocal.regenerate_shell_key( to_token() )
828
-
829
- # --
830
- # -- Decrypt and acquire the content enryption key that was created
831
- # -- during the login use case and encrypted using the intra sessionary
832
- # -- key.
833
- # --
834
- unique_id = KeyId.derive_universal_id( read_app_id(), to_token() )
835
- crumbs_db.use( unique_id )
836
- power_key = intra_key.do_decrypt_key( crumbs_db.get( INTRA_KEY_CIPHERTEXT ) )
837
-
838
- # --
839
- # -- Create a new random initialization vector (iv) to use when
840
- # -- encrypting the incoming database content before writing it
841
- # -- out to the file at the crypt filepath.
842
- # --
843
- iv_base64_chars = KeyIV.new().for_storage()
844
- crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
845
- crumbs_db.set( INDEX_DB_CRYPT_IV_KEY, iv_base64_chars )
846
- random_iv = KeyIV.in_binary( iv_base64_chars )
847
-
848
- # --
849
- # -- Now we use the content encryption (power) key and the random initialization
850
- # -- vector (iv) to first encrypt the incoming content and then to Base64 encode
851
- # -- the result. This is then written into the crypt filepath derived earlier.
852
- # --
853
- binary_ciphertext = power_key.do_encrypt_text( random_iv, app_database.to_json )
854
- binary_to_write( crypt_filepath, content_header, binary_ciphertext )
855
-
856
- end
857
-
858
-
859
- # Generate a new set of envelope breadcrumbs, derive the new envelope
860
- # filepath, then <b>encrypt</b> the raw envelope content, and write the
861
- # resulting ciphertext out into the new file.
862
- #
863
- # The important parameters in play are the
864
- #
865
- # - session token used to find the storage folder
866
- # - random envelope external ID used to name the ciphertext file
867
- # - generated random key for encrypting and decrypting the content
868
- # - generated random initialization vector (IV) for crypting
869
- # - name of the file in which the locked content is placed
870
- # - header and footer content that tops and tails the ciphertext
871
- #
872
- # @param crumbs_map [Hash]
873
- #
874
- # nothing is read from this crumbs map but 3 things are written to
875
- # it with these corresponding key names
876
- #
877
- # - random content external ID {CONTENT_EXTERNAL_ID}
878
- # - high entropy crypt key {CONTENT_ENCRYPT_KEY}
879
- # - and initialization vector {CONTENT_RANDOM_IV}
880
- #
881
- # @param content_body [String]
882
- #
883
- # this is the envelope's latest and greatest content that will
884
- # be encrypted, encoded, topped, tailed and then pushed out to
885
- # the domain's storage folder.
886
- #
887
- # @param content_header [String]
888
- #
889
- # the string that will top the ciphertext content when it is written
890
- #
891
- def self.content_lock( crumbs_map, content_body, content_header )
892
-
893
- # --
894
- # -- Create the external content ID and place
895
- # -- it within the crumbs map.
896
- # --
897
- content_exid = get_random_reference()
898
- crumbs_map[ CONTENT_EXTERNAL_ID ] = content_exid
899
-
900
- # --
901
- # -- Create a random initialization vector (iv)
902
- # -- for AES encryption and store it within the
903
- # -- breadcrumbs map.
904
- # --
905
- iv_base64 = KeyIV.new().for_storage()
906
- random_iv = KeyIV.in_binary( iv_base64 )
907
- crumbs_map[ CONTENT_RANDOM_IV ] = iv_base64
908
-
909
- # --
910
- # -- Create a new high entropy random key for
911
- # -- locking the content with AES. Place the key
912
- # -- within the breadcrumbs map.
913
- # --
914
- crypt_key = Key.from_random()
915
- crumbs_map[ CONTENT_ENCRYPT_KEY ] = crypt_key.to_char64()
916
-
917
- # --
918
- # -- Now use AES to lock the content body and write
919
- # -- the encoded ciphertext out to a file that is
920
- # -- topped with the parameter content header.
921
- # --
922
- binary_ctext = crypt_key.do_encrypt_text( random_iv, content_body )
923
- content_path = content_filepath( content_exid )
924
- binary_to_write( content_path, content_header, binary_ctext )
925
-
926
- end
927
-
928
-
929
- # Use the content's external id expected in the breadcrumbs together with
930
- # the session token to derive the content's filepath and then unlock and
931
- # the content as a {KeyDb} structure.
932
- #
933
- # Unlocking the content means reading it, decoding and then decrypting it using
934
- # the initialization vector (iv) and decryption key whose values are expected
935
- # within the breadcrumbs map.
936
- #
937
- # @param crumbs_map [Hash]
938
- #
939
- # the three (3) data points expected within the breadcrumbs map are the
940
- #
941
- # - content's external ID {CONTENT_EXTERNAL_ID}
942
- # - AES encryption key {CONTENT_ENCRYPT_KEY}
943
- # - initialization vector {CONTENT_RANDOM_IV}
944
- #
945
- def self.content_unlock( crumbs_map )
946
-
947
- # --
948
- # -- Get the external ID of the content then use
949
- # -- that plus the session context to derive the
950
- # -- content's ciphertext filepath.
951
- # --
952
- content_path = content_filepath( crumbs_map[ CONTENT_EXTERNAL_ID ] )
953
-
954
- # --
955
- # -- Read the binary ciphertext of the content
956
- # -- from the file. Then decrypt it using the
957
- # -- AES crypt key and intialization vector.
958
- # --
959
- crypt_txt = binary_from_read( content_path )
960
- random_iv = KeyIV.in_binary( crumbs_map[ CONTENT_RANDOM_IV ] )
961
- crypt_key = Key.from_char64( crumbs_map[ CONTENT_ENCRYPT_KEY ] )
962
- text_data = crypt_key.do_decrypt_text( random_iv, crypt_txt )
963
-
964
- return text_data
965
-
966
- end
967
-
968
-
969
- # This method returns the <b>content filepath</b> which (at its core)
970
- # is an amalgam of the application's (domain) identifier and the content's
971
- # external identifier (XID).
972
- #
973
- # The filename is prefixed by {CONTENT_FILE_PREFIX}.
974
- #
975
- # @param external_id [String]
976
- #
977
- # nothing is read from this crumbs map but 3 things are written to
978
- # it with these corresponding key names
979
- #
980
- # - random content external ID {CONTENT_EXTERNAL_ID}
981
- # - high entropy crypt key {CONTENT_ENCRYPT_KEY}
982
- # - and initialization vector {CONTENT_RANDOM_IV}
983
- def self.content_filepath( external_id )
984
-
985
- app_identity = read_app_id()
986
- store_folder = get_store_folder()
987
- env_filename = "#{CONTENT_FILE_PREFIX}.#{external_id}.#{app_identity}.txt"
988
- env_filepath = File.join( store_folder, env_filename )
989
- return env_filepath
990
-
991
- end
992
-
993
-
994
- # If the <b>content dictionary is not nil</b> and contains a key named
995
- # {CONTENT_EXTERNAL_ID} then we return true as we expect the content
996
- # ciphertext and its corresponding file to exist.
997
- #
998
- # This method throws an exception if they key exists but there is no
999
- # file at the expected location.
1000
- #
1001
- # @param crumbs_map [Hash]
1002
- #
1003
- # we test for the existence of the constant {CONTENT_EXTERNAL_ID}
1004
- # and if it exists we assert that the content filepath should also
1005
- # be present.
1006
- #
1007
- def self.db_envelope_exists?( crumbs_map )
1008
-
1009
- return false if crumbs_map.nil?
1010
- return false unless crumbs_map.has_key?( CONTENT_EXTERNAL_ID )
1011
-
1012
- external_id = crumbs_map[ CONTENT_EXTERNAL_ID ]
1013
- the_filepath = content_filepath( external_id )
1014
- error_string = "External ID #{external_id} found but no file at #{the_filepath}"
1015
- raise RuntimeException, error_string unless File.file?( the_filepath )
1016
-
1017
- return true
1018
-
1019
- end
1020
-
1021
-
1022
- # Construct the header for the ciphertext content files written out
1023
- # onto the filesystem.
1024
- #
1025
- # @param gem_version [String] the current version number of the calling gem
1026
- # @param gem_name [String] the current name of the calling gem
1027
- # @param gem_site [String] the current website of the calling gem
1028
- #
1029
- # @param the_domain_name [String]
1030
- #
1031
- # This method uses one of the two (2) ways to gain the application id.
1032
- #
1033
- # If not logged in callers will have the domain name and should pass it
1034
- # in so that this method can use {KeyId.derive_app_instance_identifier}
1035
- # to gain the application id.
1036
- #
1037
- # If logged in then method {KeyApi.use_application_domain} will have
1038
- # executed and the application ID will be written inside the
1039
- # <b>machine configuration file</b> under the application instance on
1040
- # machine id and referenced in turn from the {SESSION_APP_DOMAINS} map.
1041
- #
1042
- # In the above case post a NIL domain name and this method will now
1043
- # turn to {KeyApi.read_app_id} for the application id.
1044
- def self.format_header( gem_version, gem_name, gem_site, the_domain_name = nil )
1045
-
1046
- application_id = KeyId.derive_app_instance_identifier(the_domain_name) unless the_domain_name.nil?
1047
- application_id = read_app_id() if the_domain_name.nil?
1048
- universal_id = KeyId.derive_universal_id( application_id, to_token() )
1049
-
1050
- line1 = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
1051
- line2 = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
1052
- line3 = "#{gem_name} ciphertext block\n"
1053
- line4 = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
1054
- line5 = "App Ref Num := #{application_id}\n" # application domain reference
1055
- line6 = "Access Time := #{KeyNow.grab()}\n" # timestamp of the last write
1056
- line7 = "App Version := #{gem_version}\n" # this application semantic version
1057
- line8 = "Website Url := #{gem_site}\n" # app website or github url
1058
- line9 = "Session Ref := #{universal_id}\n" # application domain reference
1059
-
1060
- return line1 + line2 + line3 + line4 + line5 + line6 + line7 + line8 + line9
1061
-
1062
- end
1063
-
1064
-
1065
- # This method depends on {use_application_domain} which sets
1066
- # the application ID against the session identity so only call
1067
- # it if we are in a logged in state.
1068
- #
1069
- # NOTE this will NOT be set until the session is logged in so
1070
- # the call fails before that. For this reason do not call this
1071
- # method from outside this class. If the domain name is
1072
- # available use {KeyId.derive_app_instance_identifier} instead.
1073
- def self.read_app_id()
1074
-
1075
- aim_id = read_aim_id()
1076
- keypairs = KeyPair.new( MACHINE_CONFIG_FILE )
1077
- keypairs.use( aim_id )
1078
- return keypairs.get( APP_INSTANCE_ID_KEY )
1079
-
1080
- end
1081
-
1082
-
1083
- def self.read_aim_id()
1084
-
1085
- session_identifier = KeyId.derive_session_id( to_token() )
1086
-
1087
- keypairs = KeyPair.new( MACHINE_CONFIG_FILE )
1088
- keypairs.use( SESSION_APP_DOMAINS )
1089
- return keypairs.get( session_identifier )
1090
-
1091
- end
1092
-
1093
-
1094
- private
1095
-
1096
-
1097
- # --------------------------------------------------------
1098
- # In order to separate keys into a new gem we must
1099
- # break knowledge of this variable name and have it
1100
- # instead passed in by clients.
1101
- TOKEN_VARIABLE_NAME = "SAFE_TTY_TOKEN"
1102
- TOKEN_VARIABLE_SIZE = 152
1103
- # --------------------------------------------------------
1104
-
1105
-
1106
- MACHINE_CONFIG_FILE = File.join( Dir.home, ".config/openkey/openkey.app.config.ini" )
1107
- SESSION_APP_DOMAINS = "session.app.domains"
1108
- SESSION_IDENTIFIER_KEY = "session.identifiers"
1109
- KEYSTORE_IDENTIFIER_KEY = "keystore.url.id"
1110
- APP_INSTANCE_ID_KEY = "app.instance.id"
1111
- AIM_IDENTITY_REF_KEY = "aim.identity.ref"
1112
- LOGIN_TIMESTAMP_KEY = "login.timestamp"
1113
- LOGOUT_TIMESTAMP_KEY = "logout.timestamp"
1114
- MACHINE_CONFIGURATION = "machine.configuration"
1115
- APP_INITIALIZE_TIME = "initialize.time"
1116
-
1117
- APP_INSTANCE_SETUP_TIME = "app.instance.setup.time"
1118
-
1119
- APP_KEY_DB_NAME_PREFIX = "openkey.breadcrumbs"
1120
- FILE_CIPHERTEXT_PREFIX = "openkey.cipher.file"
1121
- OK_BASE_FOLDER_PREFIX = "openkey.store"
1122
- OK_BACKEND_CRYPT_PREFIX = "backend.crypt"
1123
-
1124
- APP_KEY_DB_DIRECTIVES = "key.db.directives"
1125
- APP_KEY_DB_CREATE_TIME_KEY = "initialize.time"
1126
- APP_KEY_DB_BREAD_CRUMBS = "openkey.bread.crumbs"
1127
-
1128
- LOGGED_IN_APP_SESSION_ID = "logged.in.app.session.id"
1129
- SESSION_LOGIN_DATETIME = "session.login.datetime"
1130
- SESSION_LOGOUT_DATETIME = "session.logout.datetime"
1131
-
1132
- INTER_KEY_CIPHERTEXT = "inter.key.ciphertext"
1133
- INTRA_KEY_CIPHERTEXT = "intra.key.ciphertext"
1134
- INDEX_DB_CRYPT_IV_KEY = "index.db.cipher.iv"
1135
-
1136
- BLOCK_64_START_STRING = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab\n"
1137
- BLOCK_64_END_STRING = "ba9876543210fedcba9876543210fedcba9876543210fedcba9876543210\n"
1138
- BLOCK_64_DELIMITER = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
1139
-
1140
- XID_SOURCE_APPROX_LEN = 11
1141
-
1142
- CONTENT_FILE_PREFIX = "tree.db"
1143
- CONTENT_EXTERNAL_ID = "content.xid"
1144
- CONTENT_ENCRYPT_KEY = "content.key"
1145
- CONTENT_RANDOM_IV = "content.iv"
1146
-
1147
- DB_CREATE_DATE = "db.create.date"
1148
- DB_DOMAIN_NAME = "db.domain.name"
1149
- DB_DOMAIN_ID = "db.domain.id"
1150
-
1151
-
1152
- def self.binary_to_write( to_filepath, content_header, binary_ciphertext )
1153
-
1154
- base64_ciphertext = Base64.encode64( binary_ciphertext )
1155
-
1156
- content_to_write =
1157
- content_header +
1158
- BLOCK_64_DELIMITER +
1159
- BLOCK_64_START_STRING +
1160
- base64_ciphertext +
1161
- BLOCK_64_END_STRING +
1162
- BLOCK_64_DELIMITER
1163
-
1164
- File.write( to_filepath, content_to_write )
1165
-
1166
- end
1167
-
1168
-
1169
- def self.binary_from_read( from_filepath )
1170
-
1171
- file_text = File.read( from_filepath )
1172
- core_data = file_text.in_between( BLOCK_64_START_STRING, BLOCK_64_END_STRING ).strip
1173
- return Base64.decode64( core_data )
1174
-
1175
- end
1176
-
1177
-
1178
- def self.get_random_reference
1179
-
1180
- # Do not forget that you can pass this through
1181
- # the derive identifier method if uniformity is
1182
- # what you seek.
1183
- #
1184
- # [ KeyId.derive_identifier( reference ) ]
1185
- #
1186
- random_ref = SecureRandom.urlsafe_base64( XID_SOURCE_APPROX_LEN ).delete("-_").downcase
1187
- return random_ref[ 0 .. ( XID_SOURCE_APPROX_LEN - 1 ) ]
1188
-
1189
- end
1190
-
1191
-
1192
- def self.get_virgin_content( domain_name )
1193
-
1194
- KeyError.not_new( domain_name, self )
1195
- app_id = KeyId.derive_app_instance_identifier( domain_name )
1196
-
1197
- initial_db = KeyDb.new()
1198
- initial_db.store( DB_CREATE_DATE, KeyNow.fetch() )
1199
- initial_db.store( DB_DOMAIN_NAME, domain_name )
1200
- initial_db.store( DB_DOMAIN_ID, app_id )
1201
- return initial_db.to_json
1202
-
1203
- end
1204
-
1205
-
1206
- def self.get_crumbs_db_from_domain_name( domain_name )
1207
-
1208
- KeyError.not_new( domain_name, self )
1209
- keystore_file = get_keystore_file_from_domain_name( domain_name )
1210
- crumbs_db = KeyPair.new( keystore_file )
1211
- crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
1212
- return crumbs_db
1213
-
1214
- end
1215
-
1216
-
1217
- def self.get_crumbs_db_from_session_token()
1218
-
1219
- keystore_file = get_keystore_file_from_session_token()
1220
- crumbs_db = KeyPair.new( keystore_file )
1221
- crumbs_db.use( APP_KEY_DB_BREAD_CRUMBS )
1222
- return crumbs_db
1223
-
1224
- end
1225
-
1226
-
1227
- def self.get_store_folder()
1228
-
1229
- aim_id = read_aim_id()
1230
- app_id = read_app_id()
1231
- return get_app_keystore_folder( aim_id, app_id )
1232
-
1233
- end
1234
-
1235
-
1236
- def self.get_app_keystore_folder( aim_id, app_id )
1237
-
1238
- keypairs = KeyPair.new( MACHINE_CONFIG_FILE )
1239
- keypairs.use( aim_id )
1240
- keystore_url = keypairs.get( KEYSTORE_IDENTIFIER_KEY )
1241
- basedir_name = "#{OK_BASE_FOLDER_PREFIX}.#{app_id}"
1242
- return File.join( keystore_url, basedir_name )
1243
-
1244
- end
1245
-
1246
-
1247
- def self.get_keystore_file_from_domain_name( domain_name )
1248
-
1249
- aim_id = KeyId.derive_app_instance_machine_id( domain_name )
1250
- app_id = KeyId.derive_app_instance_identifier( domain_name )
1251
-
1252
- app_key_db_file = "#{APP_KEY_DB_NAME_PREFIX}.#{app_id}.ini"
1253
- return File.join( get_app_keystore_folder( aim_id, app_id ), app_key_db_file )
1254
-
1255
- end
1256
-
1257
-
1258
- def self.get_keystore_file_from_session_token()
1259
-
1260
- aim_id = read_aim_id()
1261
- app_id = read_app_id()
1262
-
1263
- app_key_db_file = "#{APP_KEY_DB_NAME_PREFIX}.#{app_id}.ini"
1264
- return File.join( get_app_keystore_folder( aim_id, app_id ), app_key_db_file )
1265
-
1266
- end
1267
-
1268
-
1269
- def self.content_ciphertxt_file_from_domain_name( domain_name )
1270
-
1271
- aim_id = KeyId.derive_app_instance_machine_id( domain_name )
1272
- app_id = KeyId.derive_app_instance_identifier( domain_name )
1273
-
1274
- appdb_cipher_file = "#{FILE_CIPHERTEXT_PREFIX}.#{app_id}.txt"
1275
- return File.join( get_app_keystore_folder( aim_id, app_id ), appdb_cipher_file )
1276
-
1277
- end
1278
-
1279
-
1280
- def self.content_ciphertxt_file_from_session_token()
1281
-
1282
- aim_id = read_aim_id()
1283
- app_id = read_app_id()
1284
-
1285
- appdb_cipher_file = "#{FILE_CIPHERTEXT_PREFIX}.#{app_id}.txt"
1286
- return File.join( get_app_keystore_folder( aim_id, app_id ), appdb_cipher_file )
1287
-
1288
- end
1289
-
1290
-
1291
- def self.to_token()
1292
-
1293
- raw_env_var_value = ENV[TOKEN_VARIABLE_NAME]
1294
- raise_token_error( TOKEN_VARIABLE_NAME, "not present") unless raw_env_var_value
1295
-
1296
- env_var_value = raw_env_var_value.strip
1297
- raise_token_error( TOKEN_VARIABLE_NAME, "consists only of whitespace") if raw_env_var_value.empty?
1298
-
1299
- size_msg = "length should contain exactly #{TOKEN_VARIABLE_SIZE} characters"
1300
- raise_token_error( TOKEN_VARIABLE_NAME, size_msg ) unless env_var_value.length == TOKEN_VARIABLE_SIZE
1301
-
1302
- return env_var_value
1303
-
1304
- end
1305
-
1306
-
1307
- def self.raise_token_error env_var_name, message
1308
-
1309
- puts ""
1310
- puts "#{TOKEN_VARIABLE_NAME} environment variable #{message}."
1311
- puts "To instantiate it you can use the below command."
1312
- puts ""
1313
- puts "$ export #{TOKEN_VARIABLE_NAME}=`safe token`"
1314
- puts ""
1315
- puts "ps => those are backticks around `safe token` (not apostrophes)."
1316
- puts ""
1317
-
1318
- raise RuntimeError, "#{TOKEN_VARIABLE_NAME} environment variable #{message}."
1319
-
1320
- end
1321
-
1322
-
1323
- end
1324
-
1325
-
1326
- end