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,107 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
module OpenKey
|
4
|
+
|
5
|
+
# Create and deliver representations of a random initialization vector
|
6
|
+
# suitable for the AES symmetric encryption algorithm which demands a
|
7
|
+
# 18 byte binary string.
|
8
|
+
#
|
9
|
+
# The initialization vector is sourced from {SecureRandom} which provides
|
10
|
+
# a highly random (and secure) byte sequence usually sourced from udev-random.
|
11
|
+
#
|
12
|
+
# + ------------------ + -------- + ------------ + ------------------- +
|
13
|
+
# | Random IV Format | Bits | Bytes | Base64 |
|
14
|
+
# | ------------------ | -------- | ------------ | ------------------- |
|
15
|
+
# | Random IV Stored | 192 Bits | 24 bytes | 32 characters |
|
16
|
+
# | Random IV Binary | 128 Bits | 16 bytes | (not stored) |
|
17
|
+
# + ------------------ + -------- + ------------ + ------------------- +
|
18
|
+
#
|
19
|
+
# This table shows that the initialization vector can be represented by
|
20
|
+
# both a <b>32 character base64 string</b> suitable for storage and a
|
21
|
+
# <b>18 byte binary</b> for feeding the algorithm.
|
22
|
+
class KeyIV
|
23
|
+
|
24
|
+
|
25
|
+
# The 24 random bytes is equivalent to 192 bits which when sliced into 6 bit
|
26
|
+
# blocks (one for each base64 character) results in 32 base64 characters.
|
27
|
+
NO_OF_BASE64_CHARS = 32
|
28
|
+
|
29
|
+
# We ask for 24 secure random bytes that are individually created to ensure
|
30
|
+
# we get exactly the right number.
|
31
|
+
NO_OF_SOURCE_BYTES = 24
|
32
|
+
|
33
|
+
# We truncate the source random bytes so that 16 bytes are returned for the
|
34
|
+
# random initialization vector.
|
35
|
+
NO_OF_BINARY_BYTES = 16
|
36
|
+
|
37
|
+
|
38
|
+
# Initialize an initialization vector from a source of random bytes
|
39
|
+
# which can then be presented in both a <b>(base64) storage</b> format
|
40
|
+
# and a <b>binary string</b> format.
|
41
|
+
#
|
42
|
+
# + ------------------ + -------- + ------------ + ------------------- +
|
43
|
+
# | Random IV Format | Bits | Bytes | Base64 |
|
44
|
+
# | ------------------ | -------- | ------------ | ------------------- |
|
45
|
+
# | Random IV Stored | 192 Bits | 24 bytes | 32 characters |
|
46
|
+
# | Random IV Binary | 128 Bits | 16 bytes | (not stored) |
|
47
|
+
# + ------------------ + -------- + ------------ + ------------------- +
|
48
|
+
#
|
49
|
+
# We ask for 24 secure random bytes that are individually created to ensure
|
50
|
+
# we get exactly the right number.
|
51
|
+
#
|
52
|
+
# If the storage format is requested a <b>32 character base64 string</b> is
|
53
|
+
# returned but if the binary form is requested the <b>first 16 bytes</b> are
|
54
|
+
# issued.
|
55
|
+
def initialize
|
56
|
+
@bit_string = Key.to_random_bits( NO_OF_SOURCE_BYTES )
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# When the storage format is requested a <b>32 character base64 string</b> is
|
61
|
+
# returned - created from the initialized 24 secure random bytes.
|
62
|
+
#
|
63
|
+
# + ---------------- + -------- + ------------ + ------------------- +
|
64
|
+
# | Random IV Stored | 192 Bits | 24 bytes | 32 characters |
|
65
|
+
# + ---------------- + -------- + ------------ + ------------------- +
|
66
|
+
#
|
67
|
+
# @return [String]
|
68
|
+
# a <b>32 character base64 formatted string</b> is returned.
|
69
|
+
def for_storage
|
70
|
+
return Key64.from_bits( @bit_string )
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
#
|
75
|
+
# + ---------------- + -------- + ------------ + ------------------- +
|
76
|
+
# | Random IV Binary | 128 Bits | 16 bytes | (not stored) |
|
77
|
+
# + ---------------- + -------- + ------------ + ------------------- +
|
78
|
+
#
|
79
|
+
# @param iv_base64_chars [String]
|
80
|
+
# the 32 characters in base64 format that will be converted into a binary
|
81
|
+
# string (24 byte) representation and then truncated to 16 bytes and outputted
|
82
|
+
# in binary form.
|
83
|
+
#
|
84
|
+
# @return [String]
|
85
|
+
# a <b>16 byte binary string</b> is returned.
|
86
|
+
#
|
87
|
+
# @raise [ArgumentError]
|
88
|
+
# if a <b>32 base64 characters</b> are not presented in the parameter.
|
89
|
+
def self.in_binary iv_base64_chars
|
90
|
+
|
91
|
+
b64_msg = "Expected #{NO_OF_BASE64_CHARS} base64 chars not #{iv_base64_chars.length}."
|
92
|
+
raise ArgumentError, b64_msg unless iv_base64_chars.length == NO_OF_BASE64_CHARS
|
93
|
+
|
94
|
+
binary_string = Key.to_binary_from_bit_string( Key64.to_bits( iv_base64_chars ) )
|
95
|
+
|
96
|
+
bin_msg = "Expected #{NO_OF_SOURCE_BYTES} binary bytes not #{binary_string.length}."
|
97
|
+
raise RuntimeError, bin_msg unless binary_string.length == NO_OF_SOURCE_BYTES
|
98
|
+
|
99
|
+
return binary_string[ 0 .. ( NO_OF_BINARY_BYTES - 1 ) ]
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
module OpenKey
|
5
|
+
|
6
|
+
# The command line interface has a high entropy randomly generated
|
7
|
+
# key whose purpose is to <b>lock the application's data key</b> for
|
8
|
+
# the duration of the session which is between a login and a logout.
|
9
|
+
#
|
10
|
+
# These keys are unique to only one shell session on one workstation
|
11
|
+
# and they live lives that are no longer (and mostly shorter) than
|
12
|
+
# the life of the parent shell.
|
13
|
+
#
|
14
|
+
# == The 4 CLI Shell Entities
|
15
|
+
#
|
16
|
+
# The four (4) important entities within the shell session are
|
17
|
+
#
|
18
|
+
# - an obfuscator key for locking the shell key during a session
|
19
|
+
# - a high entropy randomly generated shell key for locking the app data key
|
20
|
+
# - one environment variable whose value embodies three (3) data segments
|
21
|
+
# - a session id derived by pushing the env var through a one-way function
|
22
|
+
class KeyLocal
|
23
|
+
|
24
|
+
|
25
|
+
# The number of Radix64 characters that make up a valid BCrypt salt.
|
26
|
+
# To create a BCrypt salt use
|
27
|
+
BCRYPT_SALT_LENGTH = 22
|
28
|
+
|
29
|
+
|
30
|
+
# The session token comprises of 3 segments with fixed lengths.
|
31
|
+
# This triply segmented text token that can be used to decrypt
|
32
|
+
# and deliver the shell key.
|
33
|
+
SESSION_TOKEN_SIZE = 128 + 22
|
34
|
+
|
35
|
+
|
36
|
+
# Given a 150 character session token, what is the index that pinpoints
|
37
|
+
# the beginning of the 22 character BCrypt salt? The answer is given
|
38
|
+
# by this BCRYPT_SALT_START_INDEX constant.
|
39
|
+
BCRYPT_SALT_START_INDEX = SESSION_TOKEN_SIZE - BCRYPT_SALT_LENGTH
|
40
|
+
|
41
|
+
|
42
|
+
# Instantiate the session by generating a random high entropy shell key
|
43
|
+
# and then generating an obfuscator key which we use to lock the shell
|
44
|
+
# key and return a triply segmented text token that can be used to decrypt
|
45
|
+
# and deliver the shell key as long as the same shell on the same machine
|
46
|
+
# is employed to make the call.
|
47
|
+
#
|
48
|
+
# <b>The 3 Session Token Segments</b>
|
49
|
+
#
|
50
|
+
# The session token is divided up into 3 segments with a total of 150
|
51
|
+
# characters.
|
52
|
+
#
|
53
|
+
# | -------- | ------------ | ------------------------------------- |
|
54
|
+
# | Segment | Length | Purpose |
|
55
|
+
# | -------- | ------------ | ------------------------------------- |
|
56
|
+
# | 1 | 16 bytes | AES Encrypt Initialization Vector(IV) |
|
57
|
+
# | 2 | 80 bytes | Cipher text from Random Key AES crypt |
|
58
|
+
# | 3 | 22 chars | Salt for obfuscator key derivation |
|
59
|
+
# | -------- | ------------ | ------------------------------------- |
|
60
|
+
# | Total | 150 chars | Session Token in Environment Variable |
|
61
|
+
# | -------- | ------------ | ------------------------------------- |
|
62
|
+
#
|
63
|
+
# Why is the <b>16 byte salt and the 80 byte BCrypt ciphertext</b> represented
|
64
|
+
# by <b>128 base64 characters</b>?
|
65
|
+
#
|
66
|
+
# 16 bytes + 80 bytes = 96 bytes
|
67
|
+
# 96 bytes x 8 bits = 768 bits
|
68
|
+
# 768 bits / 6 bits = 128 base64 characters
|
69
|
+
#
|
70
|
+
# @return [String]
|
71
|
+
# return a triply segmented text token that can be used to decrypt
|
72
|
+
# and redeliver the high entropy session shell key on the same machine
|
73
|
+
# and within the same shell on the same machine.
|
74
|
+
def self.generate_shell_key_and_token
|
75
|
+
|
76
|
+
calling_module = File.basename caller_locations(1,1).first.absolute_path, ".rb"
|
77
|
+
calling_method = caller_locations(1,1).first.base_label
|
78
|
+
calling_lineno = caller_locations(1,1).first.lineno
|
79
|
+
caller_details = "#{calling_module} | #{calling_method} | (line #{calling_lineno})"
|
80
|
+
|
81
|
+
log.info(x) { "### #####################################################################" }
|
82
|
+
log.info(x) { "### Caller Details =>> =>> #{caller_details}" }
|
83
|
+
log.info(x) { "### #####################################################################" }
|
84
|
+
|
85
|
+
|
86
|
+
bcrypt_salt_key = KdfBCrypt.generate_bcrypt_salt
|
87
|
+
obfuscator_key = derive_session_crypt_key( bcrypt_salt_key )
|
88
|
+
random_key_ciphertext = obfuscator_key.do_encrypt_key( Key.from_random() )
|
89
|
+
session_token = random_key_ciphertext + bcrypt_salt_key.reverse
|
90
|
+
assert_session_token_size( session_token )
|
91
|
+
|
92
|
+
log.info(x) { "BCrypt Salt Create => #{bcrypt_salt_key}" }
|
93
|
+
log.info(x) { "Obfuscate ShellKey => #{obfuscator_key.to_s()}" }
|
94
|
+
log.info(x) { "EncryptedKey Crypt => #{random_key_ciphertext}" }
|
95
|
+
log.info(x) { "Session Token Unit => #{session_token}" }
|
96
|
+
|
97
|
+
return session_token
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# Regenerate the random shell key that was instantiated and locked
|
103
|
+
# during the {instantiate_shell_key_and_generate_token} method.
|
104
|
+
#
|
105
|
+
# To successfully reacquire the randomly generated (and then locked)
|
106
|
+
# shell key we must be provided with four (4) data points, three (3)
|
107
|
+
# of which are embalmed within the 150 character session token
|
108
|
+
# parameter.
|
109
|
+
#
|
110
|
+
# <b>What we need to Regenerate the Shell Key</b>
|
111
|
+
#
|
112
|
+
# Regenerating the shell key is done in two steps when given the
|
113
|
+
# three (3) <b>session token segments</b> described below, and the
|
114
|
+
# shell identity key described in the {OpenKey::Identifier} class.
|
115
|
+
#
|
116
|
+
# The session token is divided up into 3 segments with a total of 150
|
117
|
+
# characters.
|
118
|
+
#
|
119
|
+
# | -------- | ------------ | ------------------------------------- |
|
120
|
+
# | Segment | Length | Purpose |
|
121
|
+
# | -------- | ------------ | ------------------------------------- |
|
122
|
+
# | 1 | 16 bytes | AES Encrypt Initialization Vector(IV) |
|
123
|
+
# | 2 | 80 bytes | Cipher text from Random Key AES crypt |
|
124
|
+
# | 3 | 22 chars | Salt 4 shell identity key derivation |
|
125
|
+
# | -------- | ------------ | ------------------------------------- |
|
126
|
+
# | Total | 150 chars | Session Token in Environment Variable |
|
127
|
+
# | -------- | ------------ | ------------------------------------- |
|
128
|
+
#
|
129
|
+
# <b>How to Regenerate the Shell Key</b>
|
130
|
+
#
|
131
|
+
# The two steps for regenerating the shell key are
|
132
|
+
#
|
133
|
+
# - use the shell identity string and the BCrypt key derivation salt
|
134
|
+
# in the third segment of the token to regenerate the shell identity
|
135
|
+
# key.
|
136
|
+
#
|
137
|
+
# - put 3 items through AES 256 decryption to derive the 256 bit shell
|
138
|
+
# crypt key. The 3 items are the <b>shell identity key</b> derived
|
139
|
+
# in step 1, the AES IV (initialization vector, in the first segment
|
140
|
+
# of the token, and the <b>ciphertext in the middle segment</b>.
|
141
|
+
#
|
142
|
+
# @param session_token [String]
|
143
|
+
# a triply segmented (and one liner) text token instantiated by
|
144
|
+
# {self.instantiate_shell_key_and_generate_token} and provided
|
145
|
+
# here ad verbatim.
|
146
|
+
#
|
147
|
+
# @return [OpenKey::Key]
|
148
|
+
# an extremely high entropy 256 bit key derived (digested) from 48
|
149
|
+
# random bytes at the beginning of the shell (cli) session.
|
150
|
+
def self.regenerate_shell_key( session_token )
|
151
|
+
|
152
|
+
calling_module = File.basename caller_locations(1,1).first.absolute_path, ".rb"
|
153
|
+
calling_method = caller_locations(1,1).first.base_label
|
154
|
+
calling_lineno = caller_locations(1,1).first.lineno
|
155
|
+
caller_details = "#{calling_module} | #{calling_method} | (line #{calling_lineno})"
|
156
|
+
|
157
|
+
log.info(x) { "### #####################################################################" }
|
158
|
+
log.info(x) { "### Caller Details =>> =>> #{caller_details}" }
|
159
|
+
log.info(x) { "### #####################################################################" }
|
160
|
+
|
161
|
+
|
162
|
+
assert_session_token_size( session_token )
|
163
|
+
bcrypt_salt = session_token[ BCRYPT_SALT_START_INDEX .. -1 ].reverse
|
164
|
+
assert_bcrypt_salt_size( bcrypt_salt )
|
165
|
+
|
166
|
+
key_ciphertext = session_token[ 0 .. ( BCRYPT_SALT_START_INDEX - 1 ) ]
|
167
|
+
obfuscator_key = derive_session_crypt_key( bcrypt_salt )
|
168
|
+
|
169
|
+
log.info(x) { "BCrypt Salt REGEND => #{bcrypt_salt}" }
|
170
|
+
log.info(x) { "SessionToken REGEN => #{session_token}" }
|
171
|
+
log.info(x) { "Chopped Ciphertext => #{key_ciphertext}" }
|
172
|
+
log.info(x) { "Obfuscate ShellKey => #{obfuscator_key.to_s()}" }
|
173
|
+
|
174
|
+
regenerated_key = obfuscator_key.do_decrypt_key( key_ciphertext )
|
175
|
+
|
176
|
+
return regenerated_key
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
# Derive a <b>short term (session scoped) encryption key</b> from the
|
182
|
+
# surrounding shell and workstation (machine) environment with an
|
183
|
+
# important same/different guarantee.
|
184
|
+
#
|
185
|
+
# The <b>same / different guarantee promises</b> us that the derived
|
186
|
+
# key will be
|
187
|
+
#
|
188
|
+
# - <b>the same</b> whenever called from within this executing shell
|
189
|
+
# - <b>different</b> when the shell and/or workstation are different
|
190
|
+
#
|
191
|
+
# This method uses a one-way function to return a combinatorial digested
|
192
|
+
# session identification string using a number of distinct parameters that
|
193
|
+
# deliver the important behaviours of changing in certain circumstances
|
194
|
+
# and remaining unchanged in others.
|
195
|
+
#
|
196
|
+
# <b>Change | When Should the key Change?</b>
|
197
|
+
#
|
198
|
+
# What is really important is that the <b>key changes when</b>
|
199
|
+
#
|
200
|
+
# - the <b>command shell</b> changes
|
201
|
+
# - the workstation <b>shell user is switched</b>
|
202
|
+
# - the host machine <b>workstation</b> is changed
|
203
|
+
# - the user <b>SSH's</b> into another shell
|
204
|
+
#
|
205
|
+
# A distinct workstation is identified by the first MAC address and the
|
206
|
+
# hostname of the machine.
|
207
|
+
#
|
208
|
+
# <b>Unchanged | When Should the Key Remain Unchanged?</b>
|
209
|
+
#
|
210
|
+
# Remaining <b>unchanged</b> in certain scenarios is a feature that is
|
211
|
+
# just as important as changing in others. The key must remain
|
212
|
+
# <b>unchanged</b> when
|
213
|
+
#
|
214
|
+
# - the <b>user returns to a command shell</b>
|
215
|
+
# - the user exits their <b>remote SSH session</b>
|
216
|
+
# - <b>sudo is used</b> to execute the commands
|
217
|
+
# - the user comes back to their <b>workstation</b>
|
218
|
+
# - the clock ticks into another day, month, year ...
|
219
|
+
#
|
220
|
+
# @param bcrypt_salt_key [OpenKey::Key]
|
221
|
+
#
|
222
|
+
# Either use the {KdfBCrypt.generate_bcrypt_salt} method to generate
|
223
|
+
# the salt or retrieve and post in a previously generated salt which
|
224
|
+
# must hold 22 printable characters.
|
225
|
+
#
|
226
|
+
# @return [OpenKey::Key]
|
227
|
+
# a digested key suitable for short term (session scoped) use with the
|
228
|
+
# guarantee that the same key will be returned whenever called from within
|
229
|
+
# the same executing shell environment and a different key when not.
|
230
|
+
def self.derive_session_crypt_key bcrypt_salt_key
|
231
|
+
|
232
|
+
shell_id_text = KeyMach.derive_shell_identity_string()
|
233
|
+
truncate_text = shell_id_text.length > KdfBCrypt::BCRYPT_MAX_IN_TEXT_LENGTH
|
234
|
+
shell_id_trim = shell_id_text unless truncate_text
|
235
|
+
shell_id_trim = shell_id_text[ 0 .. ( KdfBCrypt::BCRYPT_MAX_IN_TEXT_LENGTH - 1 ) ] if truncate_text
|
236
|
+
|
237
|
+
log.info(x) { "Shell Identity Str => #{shell_id_text}" }
|
238
|
+
log.info(x) { "Shell Id TxtLength => #{shell_id_text.length()}" }
|
239
|
+
log.info(x) { "Truncate Shell Str => #{truncate_text}" }
|
240
|
+
log.info(x) { "Resulting IDString => #{shell_id_trim}" }
|
241
|
+
|
242
|
+
return KdfBCrypt.generate_key( shell_id_trim, bcrypt_salt_key )
|
243
|
+
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
|
250
|
+
def self.assert_session_token_size session_token
|
251
|
+
err_msg = "Session token has #{session_token.length} and not #{SESSION_TOKEN_SIZE} chars."
|
252
|
+
raise RuntimeError, err_msg unless session_token.length == SESSION_TOKEN_SIZE
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
def self.assert_bcrypt_salt_size bcrypt_salt
|
257
|
+
err_msg = "Expected BCrypt salt length of #{BCRYPT_SALT_LENGTH} not #{bcrypt_salt.length}."
|
258
|
+
raise RuntimeError, err_msg unless bcrypt_salt.length == BCRYPT_SALT_LENGTH
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
module OpenKey
|
5
|
+
|
6
|
+
# This class knows how to derive information from the machine environment to aide
|
7
|
+
# in producing identifiers unique to the machine and/or workstation, with functionality
|
8
|
+
# similar to that required by licensing software.
|
9
|
+
#
|
10
|
+
# == Identity is Similar to Licensing Software | Except Deeper
|
11
|
+
#
|
12
|
+
# Deriving the identity string follows similar principles to licensing
|
13
|
+
# software that attempts to determine whether the operating environment
|
14
|
+
# is the same or different. But it goes deeper than licensing software
|
15
|
+
# as it is not only concerned about the <b>same workstation</b> - it is
|
16
|
+
# also concerned about <b>the same shell or command line interface</b>.
|
17
|
+
#
|
18
|
+
# == Known Issues
|
19
|
+
#
|
20
|
+
# The dependent macaddr gem is known to fail in scenarios where a
|
21
|
+
# VPN tunnel is active and a tell tale sign is the ifconfig command
|
22
|
+
# returning the tun0 interface rather than "eth0" or something that
|
23
|
+
# resembles "ensp21".
|
24
|
+
#
|
25
|
+
# This is one of the error messages resulting from such a case.
|
26
|
+
#
|
27
|
+
# macaddr.rb:86 from_getifaddrs undefined method pfamily (NoMethodError)
|
28
|
+
#
|
29
|
+
class KeyMach
|
30
|
+
|
31
|
+
# This method uses a one-way function to return a combinatorial digested
|
32
|
+
# session identification string using a number of distinct parameters that
|
33
|
+
# deliver the important behaviours of changing in certain circumstances
|
34
|
+
# and remaining unchanged in others.
|
35
|
+
#
|
36
|
+
# <b>Change | When Should the Session ID Change?</b>
|
37
|
+
#
|
38
|
+
# The session id is not a secret but it has to be unique due to its role
|
39
|
+
# in indexing the session envelopes and their changes.
|
40
|
+
#
|
41
|
+
# What is really important is that the <b>session id changes</b> when
|
42
|
+
#
|
43
|
+
# - the <b>domain</b> being used changes
|
44
|
+
# - the <b>command shell</b> changes
|
45
|
+
# - the user <b>switches to another workstation user</b>
|
46
|
+
# - the <b>workstation host</b> is changed
|
47
|
+
# - the <b>session token</b> environment variable changes
|
48
|
+
# - the user <b>SSH's</b> into another shell
|
49
|
+
#
|
50
|
+
# A distinct workstation is identified by the first MAC address and the
|
51
|
+
# hostname of the machine.
|
52
|
+
#
|
53
|
+
# <b>Unchanged | When Should it Remain Unchanged?</b>
|
54
|
+
#
|
55
|
+
# Remaining <b>unchanged</b> in certain scenarious is a session ID feature
|
56
|
+
# that is just as important as it changing in others.
|
57
|
+
#
|
58
|
+
# The session ID <b>must remain unchanged</b> when
|
59
|
+
#
|
60
|
+
# - the <b>user returns to a command shell</b>
|
61
|
+
# - the user <b>switches back to using a domain</b>
|
62
|
+
# - the user exits their <b>remote SSH session</b>
|
63
|
+
# - <b>sudo is used</b> to execute the commands
|
64
|
+
# - the user comes back to their <b>workstation</b>
|
65
|
+
# - the clock ticks into another day, month, year ...
|
66
|
+
#
|
67
|
+
# The pre-hash inputs are an amalgam of the below example data.
|
68
|
+
#
|
69
|
+
# Mac Address => 20cf3067dec3
|
70
|
+
# Parent PID => 5817
|
71
|
+
# Machine Host => data-cruncher
|
72
|
+
# Session Time => 18083.0310.41.796577366
|
73
|
+
#
|
74
|
+
# @return [String]
|
75
|
+
# a one line textual shell identity string
|
76
|
+
def self.derive_shell_identity_string
|
77
|
+
|
78
|
+
require 'socket'
|
79
|
+
|
80
|
+
calling_module = File.basename caller_locations(1,1).first.absolute_path, ".rb"
|
81
|
+
calling_method = caller_locations(1,1).first.base_label
|
82
|
+
calling_lineno = caller_locations(1,1).first.lineno
|
83
|
+
caller_details = "#{calling_module} | #{calling_method} | (line #{calling_lineno})"
|
84
|
+
|
85
|
+
log.info(x) { "### #####################################################################" }
|
86
|
+
log.info(x) { "### Caller Details =>> =>> #{caller_details}" }
|
87
|
+
log.info(x) { "### #####################################################################" }
|
88
|
+
|
89
|
+
|
90
|
+
log.info(x) { "Etc.getlogin() => #{Etc.getlogin()}" }
|
91
|
+
log.info(x) { "Socket.gethostname() => #{Socket.gethostname()}" }
|
92
|
+
log.info(x) { "get_net_address_digits() => #{get_net_address_digits()}" }
|
93
|
+
log.info(x) { "derive_network_identity() => #{derive_network_identity()}" }
|
94
|
+
log.info(x) { "Process.ppid.to_s() => #{Process.ppid.to_s()}" }
|
95
|
+
|
96
|
+
|
97
|
+
# Do not change the order of this data because it is reversed and
|
98
|
+
# the parent's shell ID hotly followed by the network identifier are
|
99
|
+
# the most significant data points.
|
100
|
+
identity_text = [
|
101
|
+
Etc.getlogin(),
|
102
|
+
Socket.gethostname(),
|
103
|
+
get_net_address_digits(),
|
104
|
+
derive_network_identity(),
|
105
|
+
Process.ppid.to_s()
|
106
|
+
].join.reverse
|
107
|
+
|
108
|
+
return identity_text
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# This method uses a one-way function to return a combinatorial digested
|
114
|
+
# machine identification string using a number of distinct input parameters
|
115
|
+
# to deliver the characteristic of producing the same identifier for the
|
116
|
+
# same machine, virtual machine, workstation and/or compute element, and
|
117
|
+
# reciprocally, a different one on a different machine.
|
118
|
+
#
|
119
|
+
# The userspace is also a key machine identifier so a different machine user
|
120
|
+
# generates a different identifier when all other things remain equal.
|
121
|
+
#
|
122
|
+
# Mac Address => 20cf3067dec3
|
123
|
+
# Machine User => joebloggs
|
124
|
+
# Machine Host => data-cruncher
|
125
|
+
# IP Addresses => w.x.y.z + a.b.c.d
|
126
|
+
#
|
127
|
+
# @return [String]
|
128
|
+
# a one line textual machine workstation or compute element identifier
|
129
|
+
# that is (surprisingly) different when the machine user changes.
|
130
|
+
def self.derive_machine_identity_string
|
131
|
+
|
132
|
+
require 'socket'
|
133
|
+
|
134
|
+
identity_text = [
|
135
|
+
Etc.getlogin,
|
136
|
+
Socket.gethostname(),
|
137
|
+
get_net_address_digits(),
|
138
|
+
derive_network_identity()
|
139
|
+
].join.reverse
|
140
|
+
|
141
|
+
return identity_text
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
# If the system was rebooted on April 23rd, 2018 at 22:00:16 we
|
147
|
+
# expect this method not to return <b>2018-04-23 22:00:16</b>, but
|
148
|
+
# to return the <b>8 least significant digits</b> bootup time
|
149
|
+
# digits which in this case are <b>23220016</b>.
|
150
|
+
#
|
151
|
+
# Investigate all Linux flavours to understand whether this command
|
152
|
+
# works (or is it just Ubuntu). Also does Docker return a sensible
|
153
|
+
# value here?
|
154
|
+
#
|
155
|
+
# This method is not production ready. Not only is the time within
|
156
|
+
# a small range, also the most significant digit can fluctuate up
|
157
|
+
# or down randomly (in a non-deterministic manner.
|
158
|
+
#
|
159
|
+
# @return [String] the time when the system was booted.
|
160
|
+
def self.get_bootup_time_digits
|
161
|
+
|
162
|
+
boot_time_cmd = "uptime -s"
|
163
|
+
uptime_string = %x[ #{boot_time_cmd} ]
|
164
|
+
return uptime_string.to_alphanumeric[ 6 .. -1 ]
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
# This method will return the first readable non loopback (127.0.0.1)
|
170
|
+
# IP address if the bolean {ip_address_readable?} returns true.
|
171
|
+
#
|
172
|
+
# @return [String]
|
173
|
+
# the first sensible non-loopback IP address which we know to exist.
|
174
|
+
#
|
175
|
+
# @raise RuntimeError if the IP address that we have on good authority exists, does not.
|
176
|
+
def self.get_net_address_digits
|
177
|
+
|
178
|
+
return "mT4Lq8DsG-x=@/y(_9A:]r" unless ip_address_readable?()
|
179
|
+
|
180
|
+
ip_addresses = get_address_list()
|
181
|
+
ip_addresses.each do |candidate_address|
|
182
|
+
return candidate_address.to_alphanumeric unless candidate_address.eql?( "127.0.0.1" )
|
183
|
+
end
|
184
|
+
raise RuntimeError, "Was led to expect at least one readable IP address."
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# Return a string that is the same if the logical underlying machine the
|
190
|
+
# software is running on is the same - and different if it is different.
|
191
|
+
#
|
192
|
+
# The current implementation ciphens off the last 12 characters of the gnerated
|
193
|
+
# UUID and this is guaranteed to remain constant in a manner required by
|
194
|
+
# <b>license generating tools</b>.
|
195
|
+
#
|
196
|
+
# Within a Docker container this behaviour will have to be observed
|
197
|
+
# carefully - especially when Docker Compose type tools are used
|
198
|
+
# and the container persists across the host machine reboots.
|
199
|
+
def self.derive_network_identity
|
200
|
+
|
201
|
+
require 'uuid'
|
202
|
+
|
203
|
+
the_uuid = UUID.new.generate
|
204
|
+
|
205
|
+
too_short = the_uuid.length <= NETWORK_ID_LENGTH
|
206
|
+
raise RuntimeError, "Unexpected UUID format [#{the_uuid}]" if too_short
|
207
|
+
|
208
|
+
net_id_chunk = the_uuid[ (the_uuid.length - NETWORK_ID_LENGTH) .. -1 ]
|
209
|
+
perfect_size = net_id_chunk.length == NETWORK_ID_LENGTH
|
210
|
+
size_err_msg = "Expected [ #{net_id_chunk} ] net ID length of #{NETWORK_ID_LENGTH}."
|
211
|
+
raise RuntimeError,size_err_msg unless perfect_size
|
212
|
+
|
213
|
+
return net_id_chunk
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
|
222
|
+
|
223
|
+
NETWORK_ID_LENGTH = 12
|
224
|
+
|
225
|
+
|
226
|
+
def self.ip_address_readable?
|
227
|
+
ip_addresses = get_address_list()
|
228
|
+
no_addresses = ip_addresses.length == 0 || ( ip_addresses.length == 1 && ip_addresses[0] == "127.0.0.1" )
|
229
|
+
return false if no_addresses
|
230
|
+
ip_addresses.each do |candidate_address|
|
231
|
+
return true unless candidate_address.eql?( "127.0.0.1" )
|
232
|
+
end
|
233
|
+
return false
|
234
|
+
end
|
235
|
+
|
236
|
+
|
237
|
+
def self.get_address_list
|
238
|
+
multipleAddresses = Socket.ip_address_list
|
239
|
+
stringAddressText = multipleAddresses.to_s
|
240
|
+
ipAddressRegEx = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/;
|
241
|
+
return stringAddressText.scan( ipAddressRegEx );
|
242
|
+
end
|
243
|
+
|
244
|
+
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
end
|