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.
- checksums.yaml +5 -5
- data/README.md +56 -159
- data/bin/opensecret +2 -2
- data/bin/ops +17 -2
- data/lib/extension/string.rb +14 -16
- data/lib/{interpreter.rb → interprete.rb} +53 -29
- data/lib/keytools/binary.map.rb +49 -0
- data/lib/keytools/kdf.api.rb +249 -0
- data/lib/keytools/kdf.bcrypt.rb +64 -29
- data/lib/keytools/kdf.pbkdf2.rb +92 -83
- data/lib/keytools/kdf.scrypt.rb +190 -0
- data/lib/keytools/key.64.rb +326 -0
- data/lib/keytools/key.algo.rb +109 -0
- data/lib/keytools/key.api.rb +1281 -0
- data/lib/keytools/key.db.rb +265 -0
- data/lib/keytools/{key.module.rb → key.docs.rb} +55 -0
- data/lib/keytools/key.error.rb +110 -0
- data/lib/keytools/key.id.rb +271 -0
- data/lib/keytools/key.iv.rb +107 -0
- data/lib/keytools/key.local.rb +265 -0
- data/lib/keytools/key.mach.rb +248 -0
- data/lib/keytools/key.now.rb +402 -0
- data/lib/keytools/key.pair.rb +259 -0
- data/lib/keytools/key.pass.rb +120 -0
- data/lib/keytools/key.rb +428 -298
- data/lib/keytools/keydebug.txt +295 -0
- data/lib/logging/gem.logging.rb +3 -3
- data/lib/modules/cryptology/collect.rb +20 -0
- data/lib/session/require.gem.rb +1 -1
- data/lib/usecase/cmd.rb +417 -0
- data/lib/usecase/id.rb +36 -0
- data/lib/usecase/import.rb +174 -0
- data/lib/usecase/init.rb +78 -0
- data/lib/usecase/login.rb +70 -0
- data/lib/usecase/logout.rb +30 -0
- data/lib/usecase/open.rb +126 -0
- data/lib/{interprete → usecase}/put.rb +100 -47
- data/lib/usecase/read.rb +89 -0
- data/lib/{interprete → usecase}/safe.rb +0 -0
- data/lib/{interprete → usecase}/set.rb +0 -0
- data/lib/usecase/token.rb +111 -0
- data/lib/{interprete → usecase}/use.rb +0 -0
- data/lib/version.rb +1 -1
- data/opensecret.gemspec +4 -3
- metadata +39 -33
- data/lib/exception/cli.error.rb +0 -53
- data/lib/exception/errors/cli.errors.rb +0 -31
- data/lib/interprete/begin.rb +0 -232
- data/lib/interprete/cmd.rb +0 -621
- data/lib/interprete/export.rb +0 -163
- data/lib/interprete/init.rb +0 -205
- data/lib/interprete/key.rb +0 -119
- data/lib/interprete/open.rb +0 -148
- data/lib/interprete/seal.rb +0 -129
- data/lib/keytools/digester.rb +0 -245
- data/lib/keytools/key.data.rb +0 -227
- data/lib/keytools/key.derivation.rb +0 -341
- data/lib/modules/mappers/collateral.rb +0 -282
- data/lib/modules/mappers/envelope.rb +0 -127
- data/lib/modules/mappers/settings.rb +0 -170
- data/lib/notepad/scratch.pad.rb +0 -224
- 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
|