opensecret 0.0.988 → 0.0.9925

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +56 -159
  3. data/bin/opensecret +2 -2
  4. data/bin/ops +17 -2
  5. data/lib/extension/string.rb +14 -16
  6. data/lib/{interpreter.rb → interprete.rb} +53 -29
  7. data/lib/keytools/binary.map.rb +49 -0
  8. data/lib/keytools/kdf.api.rb +249 -0
  9. data/lib/keytools/kdf.bcrypt.rb +64 -29
  10. data/lib/keytools/kdf.pbkdf2.rb +92 -83
  11. data/lib/keytools/kdf.scrypt.rb +190 -0
  12. data/lib/keytools/key.64.rb +326 -0
  13. data/lib/keytools/key.algo.rb +109 -0
  14. data/lib/keytools/key.api.rb +1281 -0
  15. data/lib/keytools/key.db.rb +265 -0
  16. data/lib/keytools/{key.module.rb → key.docs.rb} +55 -0
  17. data/lib/keytools/key.error.rb +110 -0
  18. data/lib/keytools/key.id.rb +271 -0
  19. data/lib/keytools/key.iv.rb +107 -0
  20. data/lib/keytools/key.local.rb +265 -0
  21. data/lib/keytools/key.mach.rb +248 -0
  22. data/lib/keytools/key.now.rb +402 -0
  23. data/lib/keytools/key.pair.rb +259 -0
  24. data/lib/keytools/key.pass.rb +120 -0
  25. data/lib/keytools/key.rb +428 -298
  26. data/lib/keytools/keydebug.txt +295 -0
  27. data/lib/logging/gem.logging.rb +3 -3
  28. data/lib/modules/cryptology/collect.rb +20 -0
  29. data/lib/session/require.gem.rb +1 -1
  30. data/lib/usecase/cmd.rb +417 -0
  31. data/lib/usecase/id.rb +36 -0
  32. data/lib/usecase/import.rb +174 -0
  33. data/lib/usecase/init.rb +78 -0
  34. data/lib/usecase/login.rb +70 -0
  35. data/lib/usecase/logout.rb +30 -0
  36. data/lib/usecase/open.rb +126 -0
  37. data/lib/{interprete → usecase}/put.rb +100 -47
  38. data/lib/usecase/read.rb +89 -0
  39. data/lib/{interprete → usecase}/safe.rb +0 -0
  40. data/lib/{interprete → usecase}/set.rb +0 -0
  41. data/lib/usecase/token.rb +111 -0
  42. data/lib/{interprete → usecase}/use.rb +0 -0
  43. data/lib/version.rb +1 -1
  44. data/opensecret.gemspec +4 -3
  45. metadata +39 -33
  46. data/lib/exception/cli.error.rb +0 -53
  47. data/lib/exception/errors/cli.errors.rb +0 -31
  48. data/lib/interprete/begin.rb +0 -232
  49. data/lib/interprete/cmd.rb +0 -621
  50. data/lib/interprete/export.rb +0 -163
  51. data/lib/interprete/init.rb +0 -205
  52. data/lib/interprete/key.rb +0 -119
  53. data/lib/interprete/open.rb +0 -148
  54. data/lib/interprete/seal.rb +0 -129
  55. data/lib/keytools/digester.rb +0 -245
  56. data/lib/keytools/key.data.rb +0 -227
  57. data/lib/keytools/key.derivation.rb +0 -341
  58. data/lib/modules/mappers/collateral.rb +0 -282
  59. data/lib/modules/mappers/envelope.rb +0 -127
  60. data/lib/modules/mappers/settings.rb +0 -170
  61. data/lib/notepad/scratch.pad.rb +0 -224
  62. data/lib/store-commands.txt +0 -180
@@ -0,0 +1,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