opensecret 0.0.988 → 0.0.9925
Sign up to get free protection for your applications and to get access to all the features.
- 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
|