opensecret 0.0.988 → 0.0.9925

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 (62) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +56 -159
  3. data/bin/opensecret +2 -2
  4. data/bin/ops +17 -2
  5. data/lib/extension/string.rb +14 -16
  6. data/lib/{interpreter.rb → interprete.rb} +53 -29
  7. data/lib/keytools/binary.map.rb +49 -0
  8. data/lib/keytools/kdf.api.rb +249 -0
  9. data/lib/keytools/kdf.bcrypt.rb +64 -29
  10. data/lib/keytools/kdf.pbkdf2.rb +92 -83
  11. data/lib/keytools/kdf.scrypt.rb +190 -0
  12. data/lib/keytools/key.64.rb +326 -0
  13. data/lib/keytools/key.algo.rb +109 -0
  14. data/lib/keytools/key.api.rb +1281 -0
  15. data/lib/keytools/key.db.rb +265 -0
  16. data/lib/keytools/{key.module.rb → key.docs.rb} +55 -0
  17. data/lib/keytools/key.error.rb +110 -0
  18. data/lib/keytools/key.id.rb +271 -0
  19. data/lib/keytools/key.iv.rb +107 -0
  20. data/lib/keytools/key.local.rb +265 -0
  21. data/lib/keytools/key.mach.rb +248 -0
  22. data/lib/keytools/key.now.rb +402 -0
  23. data/lib/keytools/key.pair.rb +259 -0
  24. data/lib/keytools/key.pass.rb +120 -0
  25. data/lib/keytools/key.rb +428 -298
  26. data/lib/keytools/keydebug.txt +295 -0
  27. data/lib/logging/gem.logging.rb +3 -3
  28. data/lib/modules/cryptology/collect.rb +20 -0
  29. data/lib/session/require.gem.rb +1 -1
  30. data/lib/usecase/cmd.rb +417 -0
  31. data/lib/usecase/id.rb +36 -0
  32. data/lib/usecase/import.rb +174 -0
  33. data/lib/usecase/init.rb +78 -0
  34. data/lib/usecase/login.rb +70 -0
  35. data/lib/usecase/logout.rb +30 -0
  36. data/lib/usecase/open.rb +126 -0
  37. data/lib/{interprete → usecase}/put.rb +100 -47
  38. data/lib/usecase/read.rb +89 -0
  39. data/lib/{interprete → usecase}/safe.rb +0 -0
  40. data/lib/{interprete → usecase}/set.rb +0 -0
  41. data/lib/usecase/token.rb +111 -0
  42. data/lib/{interprete → usecase}/use.rb +0 -0
  43. data/lib/version.rb +1 -1
  44. data/opensecret.gemspec +4 -3
  45. metadata +39 -33
  46. data/lib/exception/cli.error.rb +0 -53
  47. data/lib/exception/errors/cli.errors.rb +0 -31
  48. data/lib/interprete/begin.rb +0 -232
  49. data/lib/interprete/cmd.rb +0 -621
  50. data/lib/interprete/export.rb +0 -163
  51. data/lib/interprete/init.rb +0 -205
  52. data/lib/interprete/key.rb +0 -119
  53. data/lib/interprete/open.rb +0 -148
  54. data/lib/interprete/seal.rb +0 -129
  55. data/lib/keytools/digester.rb +0 -245
  56. data/lib/keytools/key.data.rb +0 -227
  57. data/lib/keytools/key.derivation.rb +0 -341
  58. data/lib/modules/mappers/collateral.rb +0 -282
  59. data/lib/modules/mappers/envelope.rb +0 -127
  60. data/lib/modules/mappers/settings.rb +0 -170
  61. data/lib/notepad/scratch.pad.rb +0 -224
  62. data/lib/store-commands.txt +0 -180
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ module OpenKey
5
+
6
+ require 'json'
7
+
8
+ # An envelope knows how to manipulate a JSON backed data structure
9
+ # (put, add etc) <b>after reading and then decrypting it</b> from a
10
+ # file and <b>before encrypting and then writing it</b> to a file.
11
+ #
12
+ # It provides behaviour to which we can create, append (add), update
13
+ # (change), read parts and delete essentially two structures
14
+ #
15
+ # - a collection of name/value pairs
16
+ # - an ordered list of values
17
+ #
18
+ # == JSON is Not Exposed in the Interface
19
+ #
20
+ # An envelope doesn't expose the data format used in the implementation
21
+ # allowing this to be changed seamlessly to YAMl or other formats.
22
+ #
23
+ # == Symmetric Encryption and Decryption
24
+ #
25
+ # An envelope supports operations to <b>read from</b> and <b>write to</b>
26
+ # a known filepath and with a symmetric key it can
27
+ #
28
+ # - decrypt <b>after reading from</b> a file and
29
+ # - encrypt <b>before writing to</b> a (the same) file
30
+ #
31
+ # == Hashes as the Primary Data Structure
32
+ #
33
+ # Envelope extends {Hash} as the core data structure for holding
34
+ #
35
+ # - strings
36
+ # - arrays
37
+ # - other hashes
38
+ # - booleans
39
+ # - integers and floats
40
+ class KeyDb < Hash
41
+
42
+ # Return a key database data structure that is instantiated from
43
+ # the parameter JSON string.
44
+ #
45
+ # @param db_json_string [String]
46
+ # this json formatted data structure will be converted into a
47
+ # a Ruby hash (map) data structure and returned.
48
+ #
49
+ # @return [KeyDb]
50
+ # a hash data structure that has been instantiated as per the
51
+ # parameter json string content.
52
+ def self.from_json( db_json_string )
53
+
54
+ data_db = KeyDb.new()
55
+ data_db.merge!( JSON.parse( db_json_string ) )
56
+ return data_db
57
+
58
+ end
59
+
60
+
61
+
62
+ # Create a new key value entry inside a dictionary with the specified
63
+ # name at the root of this database. Successful completion means the
64
+ # named dictionary will contain one more entry than it need even if it
65
+ # did not previously exist.
66
+ #
67
+ # @param dictionary_name [String]
68
+ #
69
+ # if a dictionary with this name exists at the root of the
70
+ # database add the parameter key value pair into it.
71
+ #
72
+ # if no dictionary exists then create one first before adding
73
+ # the key value pair as the first entry into it.
74
+ #
75
+ # @param key_name [String]
76
+ #
77
+ # the key part of the key value pair that will be added into the
78
+ # dictionary whose name was provided in the first parameter.
79
+ #
80
+ # @param value [String]
81
+ #
82
+ # the value part of the key value pair that will be added into the
83
+ # dictionary whose name was provided in the first parameter.
84
+ def create_entry( dictionary_name, key_name, value )
85
+
86
+ KeyError.not_new( dictionary_name, self )
87
+ KeyError.not_new( key_name, self )
88
+ KeyError.not_new( value, self )
89
+
90
+ self[ dictionary_name ] = {} unless self.has_key?( dictionary_name )
91
+ self[ dictionary_name ][ key_name ] = value
92
+
93
+ end
94
+
95
+
96
+ # Fast forward tries to walk down this database tree as far as it can
97
+ # led by the <b>forward-slash separated</b> tree_path parameter.
98
+ #
99
+ # It stops the first time it encounters a key within the tree_path that
100
+ # is not next in line from the currently walked position and returns
101
+ # the sub tree at its point.
102
+ #
103
+ # @param tree_path [String]
104
+ #
105
+ # this is a forward slash separated path that is expected to align
106
+ # perfectly with a path down this tree database.
107
+ #
108
+ # The child tree is returned as soon as a dead end is reached where
109
+ # the next branch in the tree_path does not correspond to a key at
110
+ # the walked position.
111
+ #
112
+ # The remaining subtree is returned if the tree_path end is reached.
113
+ def fast_forward( tree_path )
114
+
115
+ KeyError.not_new( tree_path, self )
116
+
117
+ ## @todo put a loop here
118
+ ## @todo put a loop here
119
+ ## @todo put a loop here
120
+ ## @todo put a loop here
121
+ ## @todo put a loop here
122
+ sub_tree = self[ tree_path ] if self.has_key?( tree_path )
123
+
124
+ sub_database = KeyDb.new()
125
+ sub_database.merge!( sub_tree )
126
+
127
+ return sub_database
128
+
129
+ end
130
+
131
+
132
+ # Does this database have an entry in the root dictionary named with
133
+ # the key_name parameter?
134
+ #
135
+ # @param dictionary_name [String]
136
+ #
137
+ # immediately return false if a dictionary with this name does
138
+ # <b>not exist</b> at the root of this database.
139
+ #
140
+ # @param key_name [String]
141
+ #
142
+ # test whether a key/value pair answering to this name exists inside
143
+ # the specified dictionary at the root of this database.
144
+ #
145
+ def has_entry?( dictionary_name, key_name )
146
+
147
+ KeyError.not_new( dictionary_name, self )
148
+ KeyError.not_new( key_name, self )
149
+
150
+ return false unless self.has_key?( dictionary_name )
151
+ return self[ dictionary_name ].has_key?( key_name )
152
+
153
+ end
154
+
155
+
156
+ # Get the entry with the key name in a dictionary that is itself
157
+ # inside another dictionary (named in the first parameter) which
158
+ # thankfully is at the root of this database.
159
+ #
160
+ # Only call this method if {has_entry?} returns true for the same
161
+ # dictionary and key name parameters.
162
+ #
163
+ # @param dictionary_name [String]
164
+ #
165
+ # get the entry inside a dictionary which is itself inside a
166
+ # dictionary (with this dictionary name) which is itself at the
167
+ # root of this database.
168
+ #
169
+ # @param key_name [String]
170
+ #
171
+ # get the value part of the key value pair that is inside a
172
+ # dictionary (with the above dictionary name) which is itself
173
+ # at the root of this database.
174
+ #
175
+ def get_entry( dictionary_name, key_name )
176
+
177
+ return self[ dictionary_name ][ key_name ]
178
+
179
+ end
180
+
181
+
182
+ # Read and inject into this envelope, the data structure found in a
183
+ # file at the path specified in the first parameter.
184
+ #
185
+ # Symmetric cryptography is mandatory for the envelope so we must
186
+ # <b>encrypt before writing</b> and <b>decrypt after reading</b>.
187
+ #
188
+ # An argument error will result if a suitable key is not provided.
189
+ #
190
+ # If the file does not exist (denoting the first read) all this method
191
+ # does is to stash the filepath as an instance variable and igore the
192
+ # decryption key which can be nil (or ommitted).
193
+ #
194
+ # @param the_filepath [String]
195
+ # absolute path to the file which acts as the persistent mirror to
196
+ # this data structure envelope.
197
+ #
198
+ # @param decryption_key [String]
199
+ # encryption at rest is a given so this mandatory parameter must
200
+ # contain a robust symmetric decryption key. The key will be used
201
+ # for decryption after the read and it will not linger (ie not cached
202
+ # as an instance variable).
203
+ #
204
+ # @raise [ArgumentError] if the decryption key is not robust enough.
205
+ def read the_filepath, decryption_key = nil
206
+
207
+ raise RuntimeError, "This KeyDb.read() software is never called so how can I be here?"
208
+
209
+ @filepath = the_filepath
210
+ return unless File.exists? @filepath
211
+
212
+ cipher_text = Base64.decode64( File.read( @filepath ).strip )
213
+ plain_text = ToolBelt::Blowfish.decryptor( cipher_text, decryption_key )
214
+
215
+ data_structure = JSON.parse plain_text
216
+ self.merge! data_structure
217
+
218
+ end
219
+
220
+
221
+ # Write the data in this envelope hash map into a file-system
222
+ # backed mirror whose path was specified in the {self.read} method.
223
+ #
224
+ # Technology for encryption at rest is supported by this dictionary
225
+ # and to this aim, please endeavour to post a robust symmetric
226
+ # encryption key.
227
+ #
228
+ # Calling this {self.write} method when the file at the prescribed path
229
+ # does not exist results in the directory structure being created
230
+ # (if necessary) and then the encrypted file being written.
231
+ #
232
+ # @param encryption_key [String]
233
+ # encryption at rest is a given so this mandatory parameter must
234
+ # contain a robust symmetric encryption key. The symmetric key will
235
+ # be used for the decryption after the read. Note that the decryption
236
+ # key does not linger meaning it isn't cached in an instance variable.
237
+ def write encryption_key
238
+
239
+ raise RuntimeError, "This KeyDb.write( key ) software is never called so how can I be here?"
240
+
241
+ FileUtils.mkdir_p(File.dirname(@filepath))
242
+ cipher_text = Base64.encode64 ToolBelt::Blowfish.encryptor( self.to_json, encryption_key )
243
+ File.write @filepath, cipher_text
244
+
245
+ puts ""
246
+ puts "=== ============================"
247
+ puts "=== Envelope State ============="
248
+ puts "=== ============================"
249
+
250
+ a_ini_file = IniFile.new
251
+ self.each_key do |section_name|
252
+ a_ini_file[section_name] = self[section_name]
253
+ end
254
+ puts a_ini_file.to_s
255
+
256
+ puts "=== ============================"
257
+ puts ""
258
+
259
+ end
260
+
261
+
262
+ end
263
+
264
+
265
+ end
@@ -135,6 +135,61 @@
135
135
  # - [2] a new master key is generated for every session only to hold the master index file
136
136
  #
137
137
  # - [3] it uses both <b>BCrypt</b> (Blowfish Crypt) and the indefatigable <b>PBKD2</b>
138
+
139
+
140
+ # After a successful initialization, the application instance is linked to a keystore
141
+ # whose contents are responsible for securing the application instance database.
142
+ #
143
+ # To ascertain what needs to be done to bridge the gap to full initialization the
144
+ # app needs to know 3 things from the KeyApi. These things are
145
+ #
146
+ # - the ID of this app instance on the machine
147
+ # - if a keystore been associated with this ID
148
+ # - whether the keystore secures the app database
149
+ #
150
+ # The answers dictate the steps that need to be undertaken to bring the database of
151
+ # the application instance under the secure wing of the KeyApi.
152
+ #
153
+ #
154
+ # == 1. What is the App Instance ID on this Machine?
155
+ #
156
+ # The KeyApi uses the "just given" application reference and the machine environment to
157
+ # respond with a <b>digested identifier</b> binding the application instance to the
158
+ # present machine (workstation).
159
+ #
160
+ #
161
+ # == 2. Has a Keystore been associated with this ID?
162
+ #
163
+ # The application's configuration manager is asked to find an associated KeyStore ID
164
+ # mapped against the app/machine id garnered by question 1.
165
+ #
166
+ # <b>No it has not!</b>
167
+ #
168
+ # If <b>NO</b> then a KeyStore ID is acquired <b>either from the init command's parameter</b>,
169
+ # or a <b>suitable default</b>. This new association between the app/machine ID and the
170
+ # KeyStore ID is then stored so the answer next time will be <b>YES</b>.
171
+ #
172
+ # <b>Yes it has!</b>
173
+ #
174
+ # Great - we now submit the KeyStore ID to the KeyApi so that it may answer question 3.
175
+ #
176
+ #
177
+ # == 3. Does the keystore secure the app instance database?
178
+ #
179
+ # For the KeyApi to answer, it needs the App's Instance ID and the KeyStore ID.
180
+ #
181
+ # <b>Not Yet!</b> Now <b>NO</b> means this application instance's database has not been
182
+ # brought under the protection of the KeyApi's multi-layered security net. For this it
183
+ # needs
184
+ #
185
+ # - the KeyStore ID
186
+ # - the application instance reference
187
+ # - the plaintext secret from which nothing of the host survives
188
+ # - the current application database plaintext
189
+ #
190
+ # <b>Yes it does!</b> If the app db keys <b>have been instantiated</b> and the client app is
191
+ # <b>sitting pretty</b> in possession of the database ciphertext, no more needs doing.
192
+
138
193
  module OpenKey
139
194
 
140
195
  end
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/ruby
2
+
3
+ module OpenKey
4
+
5
+
6
+ # This class is the parent to all opensession errors
7
+ # that originate from the command line.
8
+ #
9
+ # All opensession cli originating errors are about
10
+ #
11
+ # - a problem with the input or
12
+ # - a problem with the current state or
13
+ # - a predictable future problem
14
+ class KeyError < StandardError
15
+
16
+
17
+ # Initialize the error and provide a culprit
18
+ # object which will be to-stringed and given
19
+ # out as evidence (look at this)!
20
+ #
21
+ # This method will take care of loggin the error.
22
+ #
23
+ # @param message [String] human readable error message
24
+ # @param culprit [Object] object that is either pertinent, a culprit or culpable
25
+ def initialize message, culprit
26
+
27
+ super(message)
28
+
29
+ @the_culprit = culprit
30
+
31
+ log.info(x) { "An [Error] Occured => #{message}" }
32
+ log.info(x) { "Object of Interest => #{culprit.to_s}" } unless culprit.nil?
33
+ log.info(x) { "Class Name Culprit => #{culprit.class.name}" }
34
+ log.info(x) { "Error Message From => #{self.class.name}" }
35
+
36
+ thread_backtrace = Thread.current.backtrace.join("\n")
37
+ thread_backtrace.to_s.log_lines
38
+
39
+ end
40
+
41
+
42
+ # This method gives interested parties the object that
43
+ # is at the centre of the exception. This object is either
44
+ # very pertinent, culpable or at the very least, interesting.
45
+ #
46
+ # @return [String] string representation of culpable object
47
+ def culprit
48
+ return "No culprit identified." if @the_culprit.nil?
49
+ return @the_culprit.to_s
50
+ end
51
+
52
+
53
+ # Assert that the parameter string attribute is <b>not new</b> which
54
+ # means neither nil, nor empty nor consists solely of whitespace.
55
+ #
56
+ # The <b>NEW</b> acronym tells us that a bearer worthy of the name is
57
+ #
58
+ # - neither <b>N</b>il
59
+ # - nor <b>E</b>mpty
60
+ # - nor consists solely of <b>W</b>hitespace
61
+ #
62
+ # @param the_attribute [String]
63
+ # raise a {KeyError} if the attribute is not new.
64
+ #
65
+ # @param the_desc [String]
66
+ # a description of th attribute
67
+ #
68
+ # @raise [KeyError]
69
+ #
70
+ # The attribute cannot be <b>NEW</b>. The <b>NEW acronym</b> asserts
71
+ # that the attribute is
72
+ #
73
+ # - neither <b>N</b>il
74
+ # - nor <b>E</b>mpty
75
+ # - nor <b>W</b>hitespace only
76
+ #
77
+ def self.not_new the_attribute, the_desc
78
+
79
+ attribute_new = the_attribute.nil? || the_attribute.chomp.strip.empty?
80
+ return unless attribute_new
81
+
82
+ msg = "[the_desc] is either nil, empty or consists solely of whitespace."
83
+ raise KeyError.new( msg, the_desc )
84
+
85
+ end
86
+
87
+
88
+ end
89
+
90
+ =begin
91
+ # Throw this error if the configured safe directory points to a file.
92
+ class SafeDirectoryIsFile < OpenError::CliError; end;
93
+
94
+ # Throw this error if safe directory path is either nil or empty.
95
+ class SafeDirNotConfigured < OpenError::CliError; end;
96
+
97
+ # Throw this error if the email address is nil, empty or less than 5 characters.
98
+ class EmailAddrNotConfigured < OpenError::CliError; end;
99
+
100
+ # Throw this error if the store url is either nil or empty.
101
+ class StoreUrlNotConfigured < OpenError::CliError; end;
102
+
103
+ # Throw if "prime folder" name occurs 2 or more times in the path.
104
+ class SafePrimeNameRepeated < OpenError::CliError; end;
105
+
106
+ # Throw if "prime folder" name occurs 2 or more times in the path.
107
+ class SafePrimeNameNotAtEnd < OpenError::CliError; end;
108
+ =end
109
+
110
+ end
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+
5
+ module OpenKey
6
+
7
+
8
+ # This class derives <b>non secret but unique identifiers</b> based on different
9
+ # combinations of the <b>application, shell and machine (compute element)</b>
10
+ # references.
11
+ #
12
+ # == Identifier Are Not Secrets
13
+ #
14
+ # <b>And their starting values are retrievable</b>
15
+ #
16
+ # Note that the principle and practise of <b>identifiers is not about keeping secrets</b>.
17
+ # An identifier can easily give up its starting value/s if and when brute force is
18
+ # applied. The properties of a good iidentifier (ID) are
19
+ #
20
+ # - non repeatability (also known as uniqueness)
21
+ # - non predictability (of the next identifier)
22
+ # - containing alphanumerics (for file/folder/url names)
23
+ # - human readable (hence hyphens and separators)
24
+ # - non offensive (no swear words popping out)
25
+ #
26
+ # == Story | Identifiers Speak Volumes
27
+ #
28
+ # I told a friend what the turnover of his company was and how many clients he had.
29
+ # He was shocked and wanted to know how I had gleened this information.
30
+ #
31
+ # The invoices he sent me (a year apart). Both his invoice IDs (identifiers) and his
32
+ # user IDs where integers that counted up. So I could determine how many new clients
33
+ # he had in the past year, how many clients he had when I got the invoice, and I
34
+ # determined the turnover by guesstimating the average invoice amount.
35
+ #
36
+ # Many successful website attacks are owed to a predictable customer ID or a counter
37
+ # type session ID within the cookies.
38
+ #
39
+ # == Good Identifiers Need Volumes
40
+ #
41
+ # IDs are not secrets - but even so, a large number of properties are required
42
+ # to produce a high quality ID.
43
+ #
44
+ class KeyId
45
+
46
+
47
+ # The identity chunk length is set at four (4) which means each of the
48
+ # fabricated identifiers comprises of four character segments divided by
49
+ # hyphens. Only the <b>62 alpha-numerics ( a-z, A-Z and 0-9 )</b> will
50
+ # appear within identifiers - which maintains simplicity and provides an
51
+ # opportunity to re-iterate that <b>identifiers</b> are designed to be
52
+ # <b>unpredictable</b>, but <b>not secret</b>.
53
+ IDENTITY_CHUNK_LENGTH = 4
54
+
55
+
56
+ # A hyphen is the chosen character for dividing the identifier strings
57
+ # into chunks of four (4) as per the {IDENTITY_CHUNK_LENGTH} constant.
58
+ SEGMENT_CHAR = "-"
59
+
60
+
61
+ # Get an identifier that is <b>always the same</b> for the parameter
62
+ # application reference <b>regardless of the machine or shell</b> or
63
+ # even the machine user, coming together to make the request.
64
+ #
65
+ # The returned identifier will consist only of alphanumeric characters
66
+ # and one hyphen, plus it always starts and ends with an alphanumeric.
67
+ #
68
+ # @param app_instance_ref [String]
69
+ # the string reference of the application instance (or shard) that
70
+ # is in play and needs to be digested into a unique but not-a-secret
71
+ # identifier.
72
+ #
73
+ # @return [String]
74
+ # An identifier that is guaranteed to be the same whenever the
75
+ # same application reference is provided on any machine, using any
76
+ # user through any shell interface or command prompt.
77
+ #
78
+ # It must be different for any other application reference.
79
+ def self.derive_app_instance_identifier( app_instance_ref )
80
+ return derive_identifier( app_instance_ref )
81
+ end
82
+
83
+
84
+ # Get an identifier that is <b>always the same</b> for the application
85
+ # instance (with reference given in parameter) on <b>this machine</b>
86
+ # and is always different when either/or or both the application ref
87
+ # and machine are different.
88
+ #
89
+ # The returned identifier will consist of only alphanumeric characters
90
+ # and hyphens - it will always start and end with an alphanumeric.
91
+ #
92
+ # This behaviour draws a fine line around the concept of machine, virtual
93
+ # machine, <b>workstation</b> and/or <b>compute element</b>.
94
+ #
95
+ # <b>(aka) The AIM ID</b>
96
+ #
97
+ # Returned ID is aka the <b>Application Instance Machine (AIM)</b> Id.
98
+ #
99
+ # @param app_ref [String]
100
+ # the string reference of the application instance (or shard) that
101
+ # is being used.
102
+ #
103
+ # @return [String]
104
+ # an identifier that is guaranteed to be the same whenever the
105
+ # same application reference is provided on this machine.
106
+ #
107
+ # it must be different on another machine even when the same
108
+ # application reference is provided.
109
+ #
110
+ # It will also be different on this workstation if the application
111
+ # instance identifier provided is different.
112
+ def self.derive_app_instance_machine_id( app_ref )
113
+ return derive_identifier( app_ref + KeyMach.derive_machine_identity_string() )
114
+ end
115
+
116
+
117
+ # The <b>32 character</b> <b>universal identifier</b> bonds a digested
118
+ # <b>application state identifier</b> with the <b>shell identifier</b>.
119
+ # This method gives <b>dual double guarantees</b> to the effect that
120
+ #
121
+ # - a change in one, or in the other, or in both returns a different universal id
122
+ # - the same app state identifier in the same shell produces the same universal id
123
+ #
124
+ # <b>The 32 Character Universal Identifier</b>
125
+ #
126
+ # The universal identifier is an amalgam of two digests which can be individually
127
+ # retrieved from other methods in this class. An example is
128
+ #
129
+ # universal id => hg2x0-g3uslf-pa2bl5-09xvbd-n4wcq
130
+ # the shell id => g3uslf-pa2bl5-09xvbd
131
+ # app state id => hg2x0-n4wcq
132
+ #
133
+ # The 32 character universal identifier comprises of 18 session identifier
134
+ # characters (see {derive_session_id}) <b>sandwiched between</b>
135
+ # ten (10) digested application identifier characters, five (5) in front and
136
+ # five (5) at the back - all segmented by four (4) hyphens.
137
+ #
138
+ # @param app_reference [String]
139
+ # the chosen plaintext application reference identifier that
140
+ # is the input to the digesting (hashing) algorithm.
141
+ #
142
+ # @param session_token [String]
143
+ # a triply segmented (and one liner) text token instantiated by
144
+ # {KeyLocal.generate_shell_key_and_token} and provided
145
+ # here ad verbatim.
146
+ #
147
+ # @return [String]
148
+ # a 32 character string that cannot feasibly be repeated due to the use
149
+ # of one way functions within its derivation. The returned identifier bonds
150
+ # the application state reference with the present session.
151
+ def self.derive_universal_id( app_reference, session_token )
152
+
153
+ shellid = derive_session_id( session_token )
154
+ app_ref = derive_identifier( app_reference + shellid )
155
+ chunk_1 = app_ref[ 0 .. IDENTITY_CHUNK_LENGTH ]
156
+ chunk_3 = app_ref[ ( IDENTITY_CHUNK_LENGTH + 1 ) .. -1 ]
157
+
158
+ return "#{chunk_1}#{shellid}#{SEGMENT_CHAR}#{chunk_3}".downcase
159
+
160
+ end
161
+
162
+
163
+ # The session ID generated here is a derivative of the 150 character
164
+ # session token instantiated by {KeyLocal.generate_shell_key_and_token}
165
+ # and provided here <b>ad verbatim</b>.
166
+ #
167
+ # The algorithm for deriving the session ID is as follows.
168
+ #
169
+ # - convert the 150 characters to an alphanumeric string
170
+ # - convert the result to a bit string and then to a key
171
+ # - put the key's binary form through a 384 bit digest
172
+ # - convert the digest's output to 64 YACHT64 characters
173
+ # - remove the (on average 2) non-alphanumeric characters
174
+ # - cherry pick a spread out 12 characters from the pool
175
+ # - hiphenate the character positions five (5) and ten (10)
176
+ # - ensure the length of the resultant ID is fourteen (14)
177
+ #
178
+ # The resulting session id will look something like this
179
+ #
180
+ # g3sf-pab5-9xvd
181
+ #
182
+ # @param session_token [String]
183
+ # a triply segmented (and one liner) text token instantiated by
184
+ # {KeyLocal.generate_shell_key_and_token} and provided here ad
185
+ # verbatim.
186
+ #
187
+ # @return [String]
188
+ # a 14 character string that cannot feasibly be repeated
189
+ # within the keyspace of even a gigantic organisation.
190
+ #
191
+ # This method guarantees that the session id will always be the same when
192
+ # called by commands within the same shell in the same machine.
193
+ def self.derive_session_id( session_token )
194
+
195
+ assert_session_token_size( session_token )
196
+ random_length_id_key = Key.from_char64( session_token.to_alphanumeric )
197
+ a_384_bit_key = random_length_id_key.to_384_bit_key()
198
+ a_64_char_str = a_384_bit_key.to_char64()
199
+ base_64_chars = a_64_char_str.to_alphanumeric
200
+
201
+ id_chars_pool = KeyAlgo.cherry_picker( ID_TRI_CHUNK_LEN, base_64_chars )
202
+ id_hyphen_one = id_chars_pool.insert( IDENTITY_CHUNK_LENGTH, SEGMENT_CHAR )
203
+ id_characters = id_hyphen_one.insert( ( IDENTITY_CHUNK_LENGTH * 2 + 1 ), SEGMENT_CHAR )
204
+
205
+ err_msg = "Shell ID needs #{ID_TRI_TOTAL_LEN} not #{id_characters.length} characters."
206
+ raise RuntimeError, err_msg unless id_characters.length == ID_TRI_TOTAL_LEN
207
+
208
+ return id_characters.downcase
209
+
210
+ end
211
+
212
+
213
+ # This method returns a <b>10 character</b> digest of the parameter
214
+ # <b>reference</b> string.
215
+ #
216
+ # <b>How to Derive the 10 Character Identifier</b>
217
+ #
218
+ # So how are the 10 characters derived from the reference provided in
219
+ # the first parameter. The algorithm is this.
220
+ #
221
+ # - reverse the reference and feed it to a 256 bit digest
222
+ # - chop away the rightmost digits so that 252 bits are left
223
+ # - convert the one-zero bit str to 42 (YACHT64) characters
224
+ # - remove the (on average 1.5) non-alphanumeric characters
225
+ # - cherry pick and return <b>spread out 8 characters</b>
226
+ #
227
+ # @param reference [String]
228
+ # the plaintext reference input to the digest algorithm
229
+ #
230
+ # @return [String]
231
+ # a 10 character string that is a digest of the reference string
232
+ # provided in the parameter.
233
+ def self.derive_identifier( reference )
234
+
235
+ bitstr_256 = Key.from_binary( Digest::SHA256.digest( reference.reverse ) ).to_s
236
+ bitstr_252 = bitstr_256[ 0 .. ( BIT_LENGTH_252 - 1 ) ]
237
+ id_err_msg = "The ID digest needs #{BIT_LENGTH_252} not #{bitstr_252.length} chars."
238
+ raise RuntimeError, id_err_msg unless bitstr_252.length == BIT_LENGTH_252
239
+
240
+ id_chars_pool = Key64.from_bits( bitstr_252 ).to_alphanumeric
241
+ undivided_str = KeyAlgo.cherry_picker( ID_TWO_CHUNK_LEN, id_chars_pool )
242
+ id_characters = undivided_str.insert( IDENTITY_CHUNK_LENGTH, SEGMENT_CHAR )
243
+
244
+ min_size_msg = "Id length #{id_characters.length} is not #{(ID_TWO_CHUNK_LEN + 1)} chars."
245
+ raise RuntimeError, min_size_msg unless id_characters.length == ( ID_TWO_CHUNK_LEN + 1 )
246
+
247
+ return id_characters.downcase
248
+
249
+ end
250
+
251
+
252
+ private
253
+
254
+
255
+ ID_TWO_CHUNK_LEN = IDENTITY_CHUNK_LENGTH * 2
256
+ ID_TRI_CHUNK_LEN = IDENTITY_CHUNK_LENGTH * 3
257
+ ID_TRI_TOTAL_LEN = ID_TRI_CHUNK_LEN + 2
258
+
259
+ BIT_LENGTH_252 = 252
260
+
261
+
262
+ def self.assert_session_token_size session_token
263
+ err_msg = "Session token has #{session_token.length} and not #{KeyLocal::SESSION_TOKEN_SIZE} chars."
264
+ raise RuntimeError, err_msg unless session_token.length == KeyLocal::SESSION_TOKEN_SIZE
265
+ end
266
+
267
+
268
+ end
269
+
270
+
271
+ end