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,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
|