opensecret 0.0.988 → 0.0.9925

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