opensecret 0.0.962 → 0.0.988

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -10
  3. data/bin/opensecret +3 -4
  4. data/bin/ops +5 -0
  5. data/lib/extension/string.rb +114 -0
  6. data/lib/factbase/facts.opensecret.io.ini +9 -21
  7. data/lib/interprete/begin.rb +232 -0
  8. data/lib/interprete/cmd.rb +621 -0
  9. data/lib/{plugins/usecases/unlock.rb → interprete/export.rb} +25 -70
  10. data/lib/interprete/init.rb +205 -0
  11. data/lib/interprete/key.rb +119 -0
  12. data/lib/interprete/open.rb +148 -0
  13. data/lib/{plugins/usecases → interprete}/put.rb +19 -6
  14. data/lib/{plugins/usecases → interprete}/safe.rb +2 -1
  15. data/lib/{plugins/usecases/lock.rb → interprete/seal.rb} +24 -34
  16. data/lib/interprete/set.rb +46 -0
  17. data/lib/interprete/use.rb +43 -0
  18. data/lib/interpreter.rb +165 -0
  19. data/lib/keytools/binary.map.rb +245 -0
  20. data/lib/keytools/digester.rb +245 -0
  21. data/lib/keytools/doc.conversion.to.ones.and.zeroes.ruby +179 -0
  22. data/lib/keytools/doc.rsa.radix.binary-mapping.ruby +190 -0
  23. data/lib/keytools/doc.star.schema.strategy.txt +77 -0
  24. data/lib/keytools/doc.using.pbkdf2.kdf.ruby +95 -0
  25. data/lib/keytools/doc.using.pbkdf2.pkcs.ruby +266 -0
  26. data/lib/keytools/kdf.bcrypt.rb +180 -0
  27. data/lib/keytools/kdf.pbkdf2.rb +164 -0
  28. data/lib/keytools/key.data.rb +227 -0
  29. data/lib/keytools/key.derivation.rb +341 -0
  30. data/lib/keytools/key.module.rb +140 -0
  31. data/lib/keytools/key.rb +481 -0
  32. data/lib/logging/gem.logging.rb +1 -2
  33. data/lib/modules/cryptology.md +43 -0
  34. data/lib/{plugins/ciphers → modules/cryptology}/aes-256.rb +6 -0
  35. data/lib/{crypto → modules/cryptology}/amalgam.rb +6 -0
  36. data/lib/modules/cryptology/blowfish.rb +130 -0
  37. data/lib/modules/cryptology/cipher.rb +207 -0
  38. data/lib/modules/cryptology/collect.rb +118 -0
  39. data/lib/{plugins → modules/cryptology}/crypt.io.rb +5 -0
  40. data/lib/{crypto → modules/cryptology}/engineer.rb +7 -1
  41. data/lib/{crypto → modules/cryptology}/open.bcrypt.rb +0 -0
  42. data/lib/modules/mappers/collateral.rb +282 -0
  43. data/lib/modules/mappers/dictionary.rb +288 -0
  44. data/lib/modules/mappers/envelope.rb +127 -0
  45. data/lib/modules/mappers/settings.rb +170 -0
  46. data/lib/modules/storage/coldstore.rb +186 -0
  47. data/lib/{opensecret/plugins.io/git/git.flow.rb → modules/storage/git.store.rb} +11 -0
  48. data/lib/notepad/scratch.pad.rb +17 -0
  49. data/lib/session/fact.finder.rb +13 -0
  50. data/lib/session/require.gem.rb +5 -0
  51. data/lib/store-commands.txt +180 -0
  52. data/lib/version.rb +1 -1
  53. data/opensecret.gemspec +5 -6
  54. metadata +74 -29
  55. data/lib/crypto/blowfish.rb +0 -85
  56. data/lib/crypto/collect.rb +0 -140
  57. data/lib/crypto/verify.rb +0 -33
  58. data/lib/opensecret.rb +0 -236
  59. data/lib/plugins/cipher.rb +0 -203
  60. data/lib/plugins/ciphers/blowfish.rb +0 -126
  61. data/lib/plugins/coldstore.rb +0 -181
  62. data/lib/plugins/envelope.rb +0 -116
  63. data/lib/plugins/secrets.uc.rb +0 -94
  64. data/lib/plugins/usecase.rb +0 -239
  65. data/lib/plugins/usecases/init.rb +0 -145
  66. data/lib/plugins/usecases/open.rb +0 -108
  67. data/lib/session/attributes.rb +0 -279
  68. data/lib/session/dictionary.rb +0 -191
  69. data/lib/session/file.path.rb +0 -53
  70. data/lib/session/session.rb +0 -80
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ module OpenKey
5
+
6
+ require 'inifile'
7
+
8
+ # This is a key-value store backed by unencrypted (plain-text) permanent
9
+ # file-system storage in INI format.
10
+ #
11
+ # == Key-Value Pair Groupings
12
+ #
13
+ # The key-value pairs can be collated into a
14
+ #
15
+ # - custom group with a name specified to methods {read} and {write}
16
+ # - default group that is accessible via the methods {get} and {put}
17
+ #
18
+ # The name given to the default group can be specified to the constructor.
19
+ # If none is provided the aptly named "default" is used.
20
+
21
+
22
+ # An OpenSession dictionary is a <b>2D (two dimensional) hash</b> data
23
+ # structure backed by an encrypted file.
24
+ #
25
+ # It supports operations to <b>read from</b> and <b>write to</b> a known
26
+ # filepath and given the correct symmetric encryption key it will
27
+ #
28
+ # - decrypt <b>after reading from</b> the file and
29
+ # - encrypt <b>before writing to</b> the file
30
+ #
31
+ # This dictionary extends {Hash} in order to deliver on its core key value
32
+ # storage and retrieve use cases. Extend this dictionary and provide
33
+ # context specific methods through constants to read and write context
34
+ # specific data.
35
+ #
36
+ # == The <em>Current</em> Dictionary Section
37
+ #
38
+ # This KeyData is <b>two-dimensional</b> so all key-value pairs are stored
39
+ # under the auspices of a section.
40
+ #
41
+ # The KeyData can track the <b>current section</b> for you and all data
42
+ # exchanges can occur in lieu of a single section if you so wish by using
43
+ # the provided {put} and {get} methods.
44
+ #
45
+ # To employ section management functionality you should pass in a current
46
+ # <b>section id</b> when creating the dictionary.
47
+ #
48
+ # @example
49
+ # To use the dictionary in the raw (unextended) format you create
50
+ # write and read it like this.
51
+ #
52
+ # ----------------------------------------------------------------------
53
+ #
54
+ # my_dictionary = KeyData.create( "/path/to/backing/file" )
55
+ #
56
+ # my_dictionary["user23"] = {}
57
+ # my_dictionary["user23"]["Name"] = "Joe Bloggs"
58
+ # my_dictionary["user23"]["Email"] = "joebloggs@example.com"
59
+ # my_dictionary["user23"]["Phone"] = "+44 07342 800080"
60
+ #
61
+ # my_dictionary.write( "crypt-key-1234-wxyz" )
62
+ #
63
+ # ----------------------------------------------------------------------
64
+ #
65
+ # my_dictionary = KeyData.create( "/path/to/backing/file", "crypt-key-1234-wxyz" )
66
+ # puts my_dictionary.has_key? "user23" # => true
67
+ # puts my_dictionary["user23"].length # => 3
68
+ # puts my_dictionary["user23"]["Email"] # => "joebloggs@example.com"
69
+ #
70
+ # ----------------------------------------------------------------------
71
+ class KeyData
72
+
73
+
74
+ # Initialize the key value store and auto write a time stamp that
75
+ # has nano-second accuracy with a key whose name is gleened from
76
+ # the constant {KeyData::INIT_TIME_STAMP_NAME}.
77
+ #
78
+ # The path to the backing INI file is gleened from the first
79
+ # backing file path parameter.
80
+ #
81
+ # @param backing_file_path [String]
82
+ # the expected location of the file-backed key-value store.
83
+ # If the folder and/or file do not exist the folder is created
84
+ # and then the file is created along with the time stamps.
85
+ #
86
+ # @param the_default_group [String]
87
+ # the name of the default group. If none is presented this value
88
+ # will default to the aptly named "default".
89
+ def initialize backing_file_path, the_reference
90
+
91
+ @file_path = backing_file_path
92
+ @reference = the_reference
93
+
94
+ create_dir_if_necessary
95
+ put_stamps_if_necessary
96
+
97
+ end
98
+
99
+
100
+ # Stash the setting directive and its value into the configuration file
101
+ # using the default settings group.
102
+ #
103
+ # The default settings group is resolved via {Collateral::CONTEXT_NAME}
104
+ #
105
+ # @param key_name [String] the name of the key whose value is to be written
106
+ # @param key_value [String] the data item value of the key specified
107
+ def put key_name, key_value
108
+ write @reference, key_name, key_value
109
+ end
110
+
111
+
112
+ # Stash the setting directive and its value into the configuration file
113
+ # using the default settings group.
114
+ #
115
+ # The default settings group is resolved via {Collateral::CONTEXT_NAME}
116
+ #
117
+ # @param key_name [String] the name of the key whose value is to be written
118
+ # @return [String]
119
+ # return the value of the configuration directive in the default group
120
+ def get key_name
121
+ read @reference, key_name
122
+ end
123
+
124
+
125
+ # Write the key/value pair in the parameter into this key/value store's
126
+ # base file-system backing INI file.
127
+ #
128
+ # This method assumes the existence of the backing configuration file at
129
+ # the @file_path instance variable that was set during initialization.
130
+ #
131
+ # Observable value is the written key/value pair within the specified
132
+ # section. The alternate flows are
133
+ #
134
+ # - if the section does not exist it is created
135
+ # - if the section and key exist the value is inserted or overwritten
136
+ #
137
+ # @param section_name [String] name grouping the section of config values
138
+ # @param key [String] the key name of config directive to be written into the file
139
+ # @param value [String] value of the config directive to be written into the file
140
+ #
141
+ def write section_name, key, value
142
+
143
+ config_map = IniFile.new( :filename => @file_path, :encoding => 'UTF-8' )
144
+ config_map = IniFile.load( @file_path ) if File.file? @file_path
145
+ config_map[section_name][key] = value
146
+ config_map.write
147
+
148
+ end
149
+
150
+
151
+ # Given the configuration key name and the context name, get the
152
+ # corresponding key value from the configuration file whose path
153
+ # is acquired using the {self#get_filepath} method.
154
+ #
155
+ # @param key_name [String] the key whose value is to be retrieved
156
+ #
157
+ # @return [String] the value configured for the parameter key
158
+ #
159
+ # @raise ArgumentError for any one of a long list of reasons that
160
+ # cause the key value to not be retrieved. This can range from
161
+ # non-existent directories and files, non readable files, incorrect
162
+ # configurations right down to missing keys or even missing values.
163
+ def read section_name, key_name
164
+
165
+ raise ArgumentError.new "No configuration file found => [ #{@file_path} ]" unless File.exists? @file_path
166
+
167
+ the_text = File.read @file_path
168
+ raise ArgumentError.new "Configuration file is empty => [ #{@file_path} ]" if the_text.empty?
169
+
170
+ the_data = IniFile.load @file_path
171
+ key_exists = the_data[ section_name ].has_key?( key_name )
172
+ raise ArgumentError.new "Key [#{key_name}] not found in section [#{section_name}] => #{the_data.to_s}" unless key_exists
173
+
174
+ rawvalue = the_data[section_name][key_name]
175
+ raise ArgumentError.new "Empty value 4 key [#{section_name}][#{key_name}] => #{the_data.to_s}" if rawvalue.empty?
176
+
177
+ keyvalue = rawvalue.chomp.strip
178
+ raise ArgumentError.new "Whitespace value 4 key [#{section_name}][#{key_name}] => #{the_data.to_s}" if keyvalue.empty?
179
+
180
+ return keyvalue
181
+
182
+ end
183
+
184
+
185
+ # Get the time stamp that was written to the key-value store at
186
+ # the point it was first initialized and then subsequently written
187
+ # out (serialized) onto the file-system.
188
+ #
189
+ # The time stamp returned marks the first time this key-value store
190
+ # was conceived by a use case actor and subsequently serialized.
191
+ #
192
+ # @return [String]
193
+ # the string time stamp denoting the first time this key-value
194
+ # store was first initialized and then subsequently written out
195
+ # (serialized) onto the file-system.
196
+ def time_stamp
197
+ return get INIT_TIME_STAMP_NAME
198
+ end
199
+
200
+
201
+
202
+ private
203
+
204
+
205
+
206
+ def create_dir_if_necessary
207
+
208
+ config_directory = File.dirname @file_path
209
+ return if (File.exist? config_directory) && (File.directory? config_directory)
210
+ FileUtils.mkdir_p config_directory
211
+
212
+ end
213
+
214
+
215
+ def put_stamps_if_necessary
216
+
217
+ return if File.file? @file_path
218
+
219
+ put INIT_TIME_STAMP_NAME, OpenSession::Stamp.yyjjj_hhmm_ss_nanosec
220
+
221
+ end
222
+
223
+
224
+ end
225
+
226
+
227
+ end
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ module OpenKey
5
+
6
+
7
+ # The OpenKey underlying security strategy is to lock a master index file
8
+ # with a <b>symmetric encryption key</b> that is based on two randomly generated
9
+ # and amalgamated <b>55 and 45 character keys</b> and then to lock that key
10
+ # <b>(and only that key)</b> with a 256 bit symmetric encryption key derived from
11
+ # a human password and generated by at least two cryptographic workhorses known
12
+ # as <b>key derivation functions</b>.
13
+ #
14
+ # Random powerful keys are derived are seeded with 55 random bytes and
15
+ # then fed through the master key generator and its two key derivation
16
+ # functions (BCrypt and PBKDF2).
17
+ #
18
+ # == What Does the Master Encryption Key Generator Do?
19
+ #
20
+ # This class sits at the core of implementing that strategy and works to produce
21
+ # 256 bit encryption key derived from a human password which is then minced by
22
+ # two best of breed key derivation functions (BCrypt and PBKDF2).
23
+ #
24
+ # BCrypt (Blowfish) and PBKDF2 are the leading <b>key derivation functions</b>
25
+ # whose modus operandi is to convert <b>low entropy</b> human generated passwords
26
+ # into a high entropy key that is computationally infeasible to acquire via brute
27
+ # force.
28
+ #
29
+ # == How to Create the Encryption Key
30
+ #
31
+ # To create a high entropy encryption key this method takes the first
32
+ # 168 bits from the 186 bit BCrypt key produced by {BCryptKeyGen} and
33
+ # the first 96 bits from the 132 bit PBKDF2 key produced inside the
34
+ # {Pbkdf2KeyGen} class and amalgamates them to produce a 264 bit key.
35
+ #
36
+ # The 264 bit key is then digested to produce a 256bit encryption key.
37
+ class KeyDerivation
38
+
39
+
40
+ # BCrypt (Blowfish) and PBKDF2 are the leading <b>key derivation functions</b>
41
+ # whose modus operandi is to convert <b>low entropy</b> human generated passwords
42
+ # into a high entropy key that is computationally infeasible to acquire via brute
43
+ # force.
44
+ BCRYPT_SALT_KEY_NAME = "bcrypt.salt"
45
+
46
+
47
+ # BCrypt (Blowfish) and PBKDF2 are the leading <b>key derivation functions</b>
48
+ # whose modus operandi is to convert <b>low entropy</b> human generated passwords
49
+ # into a high entropy key that is computationally infeasible to acquire via brute
50
+ # force.
51
+ PBKDF2_SALT_KEY_NAME = "pbkdf2.salt"
52
+
53
+
54
+ # To create a high entropy encryption key the first 168 bits from the
55
+ # 186 bit BCrypt key produced by {BCryptKeyGen} is sliced off and used
56
+ # as the lead part of the generated key.
57
+ BCRYPT_KEY_CONTRIBUTION_SIZE = 168
58
+
59
+
60
+ # The first 96 bits from the 132 bit PBKDF2 key produced inside the
61
+ # {Pbkdf2KeyGen} class is amalgamated to the BCrypt 168 bit key to produce
62
+ # a 264 bit key.
63
+ PBKDF2_KEY_CONTRIBUTION_SIZE = 96
64
+
65
+
66
+ AMALGAM_KEY_RAW_BIT_SIZE = BCRYPT_KEY_CONTRIBUTION_SIZE + PBKDF2_KEY_CONTRIBUTION_SIZE
67
+
68
+ AMALGAM_KEY_SIX_BIT_COUNT = AMALGAM_KEY_RAW_BIT_SIZE / 6
69
+
70
+ AMALGAM_KEY_EIGHT_BYTE_COUNT = AMALGAM_KEY_RAW_BIT_SIZE / 8
71
+
72
+
73
+ # To acquire a <b>machine generated symmetric encryption key</b> pass
74
+ # in a {Key} initialized with {Key.from_random_bytes} and this method
75
+ # will digest it for extra security and produce a gold standard 256 bit
76
+ # encryption key ready to use with the AES256 algorithm.
77
+ #
78
+ # Do not use the {Key.from_random_bytes} as an encryption key, instead
79
+ # <b>encrypt and then persist</b> the key if you will need to decrypt the
80
+ # cipher text at some future date.
81
+ #
82
+ # <b>The 48 Bytes map to 64 Base64 Characters</b>
83
+ #
84
+ # To re-acquire the key for decryption, <b>read and unencrypt</b> the
85
+ # <b>64 base64 characters</b> with <b>Key.from_base64</b> and then pass
86
+ # it again to this method to <b>re-acquire</b> the original symmetric
87
+ # encryption/decryption key.
88
+ #
89
+ # | -------- | ------------ | -------------------------------- |
90
+ # | Bits | Bytes | Base64 |
91
+ # | -------- | ------------ | -------------------------------- |
92
+ # | 384 Bits | is 48 bytes | and 64 characters |
93
+ # | 256 Bits | 32 precisely | 43 Chars (42 + 4 remainder bits) |
94
+ # | -------- | ------------ | -------------------------------- |
95
+ #
96
+ # For <b>simplicity's sake</b>, try to employ a <b>bit length</b> with
97
+ # a bit count that is a <b>multiple of both 6 and 8</b>. This method mashes
98
+ # up the raw key and provides you with a powerful 256 bit key.
99
+ #
100
+ # @param the_key [OpenKey::Key]
101
+ # use <b>Key.from_random_bytes</b> to create a seed whose bit length
102
+ # is a <b>multiple of <em>both 6 and 8</em></b>. This method will mash
103
+ # up the raw key, thus provisioning a powerful 256 bit encryption key.
104
+ #
105
+ # @return [OpenKey::Key]
106
+ # the raw key will be mashed up and this method will faithfully return
107
+ # a powerful 256 bit encryption key.
108
+ def self.from_key the_key
109
+ return Digest::SHA256.digest( Digest::SHA384.digest( the_key.to_binary_bytes ) )
110
+ end
111
+
112
+
113
+
114
+ # This method generates a 256 bit symmetric encryption key derived from
115
+ # a human password and passed through two cryptographic workhorses
116
+ # (BCrypt and PBKDF2), the best of breed <b>key derivation functions</b>.
117
+ #
118
+ # == BCrypt and the PBKDF2 Cryptographic Algorithms
119
+ #
120
+ # BCrypt (Blowfish) and PBKDF2 are the leading <b>key derivation functions</b>
121
+ # that exists to convert <b>low entropy</b> human generated passwords into a high
122
+ # entropy key that is computationally infeasible to acquire through brute force.
123
+ #
124
+ # == Creating a High Entropy Encryption Key
125
+ #
126
+ # To create a high entropy encryption key this method takes the first
127
+ # 168 bits from the 186 bit BCrypt key produced by {BCryptKeyGen} and
128
+ # the first 96 bits from the 132 bit PBKDF2 key produced inside the
129
+ # {Pbkdf2KeyGen} class and amalgamates them to produce a 264 bit key.
130
+ #
131
+ # Note that all four of the above numbers are divisable by six (6), for
132
+ # representation with a 64 character set, and eight (8), for transport
133
+ # via the byte (8 bit) protocols.
134
+ #
135
+ # <b>Size of BCrypt and PBKDF2 Derived Keys</b>
136
+ #
137
+ # ----------- | --------- | ----------------- | ----------- |
138
+ # ----------- | --------- | ----------------- | ----------- |
139
+ # | Algorithm | Bit Count | Base64 Chars | 8 Bit Bytes |
140
+ # ----------- | --------- | ----------------- | ----------- |
141
+ # | BCrypt | 168 Bits | 28 characters | 21 bytes |
142
+ # | Pbkdf2 | 96 Bits | 16 characters | 12 bytes |
143
+ # ----------- | --------- | ----------------- | ----------- |
144
+ # | Total | 264 Bits | 44 characters | 33 bytes |
145
+ # ----------- | --------- | ----------------- | ----------- |
146
+ #
147
+ # <b>256 Bit Encryption Key | Remove 8 Bits</b>
148
+ #
149
+ # The manufactured encryption key, an amalgam of the above now has
150
+ # 264 bits carried by 44 Base64 characters.
151
+ #
152
+ # Just before it is used to encrypt vital keys, eight (8) bits are
153
+ # removed from the end of the key. The key is then converted into a
154
+ # powerful 32 byte (256 bit) encryption agent and is hashed by the
155
+ # SHA256 digest and delivered.
156
+ #
157
+ # @param human_secret [String]
158
+ # a robust human generated password with as much entropy as can
159
+ # be mustered. Remember that 40 characters spread randomly over
160
+ # the key space of about 90 characters and not relating to any
161
+ # dictionary word or name is the way to generate a powerful key
162
+ # that has embedded a near 100% entropy rating.
163
+ #
164
+ # @param dictionary [Hash]
165
+ # an instantiated hash object in which we will write the salts to
166
+ # be persisted and regurgitated during the regenerate process.
167
+ #
168
+ # @return [Key]
169
+ # the 256 bit symmetric encryption key derived from a human password
170
+ # and passed through two cryptographic workhorses.
171
+ def self.from_password human_secret, dictionary
172
+
173
+ bcrypt_salt = BCryptKeyGen.generate_salt
174
+ pbkdf2_salt = Pbkdf2KeyGen.generate_salt
175
+
176
+ dictionary.put( BCRYPT_SALT_KEY_NAME, bcrypt_salt )
177
+ dictionary.put( PBKDF2_SALT_KEY_NAME, pbkdf2_salt )
178
+
179
+ return generate_from_secret_and_salts human_secret, bcrypt_salt, pbkdf2_salt
180
+
181
+ end
182
+
183
+
184
+ # Regenerate the viciously unretrievable nor reversable key that was
185
+ # generated in the past and with the same salts that were used during
186
+ # the original key derivation process.
187
+ #
188
+ # @param dictionary [Hash]
189
+ # an instantiated and populated hash object containing the salts
190
+ # which were created in the past during the generation. These are
191
+ # now vital for a successful regeneration.
192
+ #
193
+ # @return [Key]
194
+ # the 256 bit symmetric encryption key that was previously generated
195
+ # from the secret and the cryptographic salts within the dictionary.
196
+ def self.regenerate human_secret, dictionary
197
+
198
+ bcrypt_salt = dictionary.get( BCRYPT_SALT_KEY_NAME )
199
+ pbkdf2_salt = dictionary.get( PBKDF2_SALT_KEY_NAME )
200
+ return generate_from_secret_and_salts human_secret, bcrypt_salt, pbkdf2_salt
201
+
202
+ end
203
+
204
+
205
+ # Derive a <b>short term (session scoped) encryption key</b> from the
206
+ # surrounding shell execution environment whilst giving two (2) important
207
+ # guarantees.
208
+ #
209
+ # The two guarantees governing the returned key are that it is
210
+ #
211
+ # - <b>the same</b> whenever called within this executing shell
212
+ # - <b>different</b> different when another shell is employed
213
+ #
214
+ # The much higher collision rate is tolerable because the key's lifetime
215
+ # is only <b>as long as commands are being typed into a given shell</b>
216
+ # or command prompt in the case of Windows.
217
+ #
218
+ # This method uses a one-way function to return a combinatorial digested
219
+ # session identification string using a number of distinct parameters that
220
+ # deliver the important behaviours of changing in certain circumstances
221
+ # and remaining unchanged in others.
222
+ #
223
+ # <b>Change | When Should the key Change?</b>
224
+ #
225
+ # What is really important is that the <b>key changes when</b>
226
+ #
227
+ # - the <b>command shell</b> changes
228
+ # - the workstation <b>shell user is switched</b>
229
+ # - the host machine <b>workstation</b> is changed
230
+ # - the user <b>SSH's</b> into another shell
231
+ #
232
+ # A distinct workstation is identified by the first MAC address and the
233
+ # hostname of the machine.
234
+ #
235
+ # <b>Unchanged | When Should the Key Remain Unchanged?</b>
236
+ #
237
+ # Remaining <b>unchanged</b> in certain scenarios is a feature that is
238
+ # just as important as changing in others. The key must remain
239
+ # <b>unchanged</b> when
240
+ #
241
+ # - the <b>user returns to a command shell</b>
242
+ # - the user exits their <b>remote SSH session</b>
243
+ # - <b>sudo is used</b> to execute the commands
244
+ # - the user comes back to their <b>workstation</b>
245
+ # - the clock ticks into another day, month, year ...
246
+ #
247
+ # @return [OpenKey::Key]
248
+ # a digested key suitable for short term (session scoped) use with the
249
+ # guarantee that the same key will be returned whenever called from within
250
+ # the same executing shell environment and a different key when not.
251
+ def self.from_session
252
+
253
+ require 'macaddr'
254
+
255
+ # Do not change the order of this data because it is reversed and
256
+ # the parent's shell ID hotly followed by the MAC address are the
257
+ # most significant data points.
258
+
259
+ raw_data_string = [
260
+ Socket.gethostname,
261
+ OpenSession::Home.instance.username,
262
+ Mac.addr.to_alphanumeric,
263
+ Process.ppid.to_s
264
+ ].join.reverse
265
+
266
+ return Digest::SHA256.digest( Digest::SHA512.digest( raw_data_string ) )
267
+
268
+ end
269
+
270
+
271
+
272
+ private
273
+
274
+
275
+
276
+ def self.generate_from_secret_and_salts human_secret, bcrypt_salt, pbkdf2_salt
277
+ bcrypt_key = OpenKey::BCryptKeyGen.generate_key( human_secret, bcrypt_salt )
278
+ pbkdf2_key = OpenKey::Pbkdf2KeyGen.generate_key( human_secret.reverse, pbkdf2_salt )
279
+ return merge_then_digest( bcrypt_key, pbkdf2_key )
280
+
281
+ end
282
+
283
+
284
+ def self.merge_then_digest bcrypt_key, pbkdf2_key
285
+
286
+ assert_bcrypt_key_bit_length bcrypt_key
287
+ assert_pbkdf2_key_bit_length pbkdf2_key
288
+
289
+ raw_key = bcrypt_key.to_s[ 0 .. (BCRYPT_KEY_CONTRIBUTION_SIZE-1) ] + pbkdf2_key.to_s[ 0 .. (PBKDF2_KEY_CONTRIBUTION_SIZE-1) ]
290
+
291
+ assert_amalgam_key_bit_length raw_key
292
+ assert_amalgam_key_six_bit_count raw_key
293
+ assert_amalgam_key_eight_bit_count raw_key
294
+
295
+ rawbytes_key = [ raw_key.to_s ].pack("B*")
296
+ digested_key = OpenSSL::Digest::SHA256.new.digest( rawbytes_key )
297
+ return Key.new ( Base64.urlsafe_encode64( digested_key ) )
298
+
299
+ end
300
+
301
+
302
+ def self.assert_bcrypt_key_bit_length bcrypt_key
303
+ bcrypt_key_bit_length = bcrypt_key.to_s.bytesize
304
+ bcrypt_keysize_msg = "Expecting #{BCryptKeyGen::BCRYPT_KEY_TRANSPORT_LENGTH} not #{bcrypt_key_bit_length} bits in bcrypt key."
305
+ raise RuntimeError, bcrypt_keysize_msg unless bcrypt_key_bit_length == BCryptKeyGen::BCRYPT_KEY_TRANSPORT_LENGTH
306
+ end
307
+
308
+
309
+ def self.assert_pbkdf2_key_bit_length pbkdf2_key
310
+ pbkdf2_key_bit_length = pbkdf2_key.to_s.bytesize
311
+ pbkdf2_keysize_msg = "Expecting #{Pbkdf2KeyGen::PBKDF2_KEY_TRANSPORT_LENGTH} not #{pbkdf2_key_bit_length} bits in pbkdf2 key."
312
+ raise RuntimeError, pbkdf2_keysize_msg unless pbkdf2_key_bit_length == Pbkdf2KeyGen::PBKDF2_KEY_TRANSPORT_LENGTH
313
+ end
314
+
315
+
316
+ def self.assert_amalgam_key_bit_length amalgam_key
317
+
318
+ amalgam_key_bit_length = amalgam_key.to_s.bytesize
319
+ amalgam_keysize_msg = "Expecting #{AMALGAM_KEY_RAW_BIT_SIZE} not #{amalgam_key_bit_length} bits in amalgam key."
320
+ raise RuntimeError, amalgam_keysize_msg unless amalgam_key_bit_length == AMALGAM_KEY_RAW_BIT_SIZE
321
+ end
322
+
323
+
324
+ def self.assert_amalgam_key_six_bit_count amalgam_key
325
+ amalgam_key_six_bit_length = amalgam_key.to_s.bytesize / 6
326
+ amalgam_key_six_bit_msg = "Expecting #{AMALGAM_KEY_SIX_BIT_COUNT} six bit blocks not #{amalgam_key_six_bit_length}."
327
+ raise RuntimeError, amalgam_key_six_bit_msg unless amalgam_key_six_bit_length == AMALGAM_KEY_SIX_BIT_COUNT
328
+ end
329
+
330
+
331
+ def self.assert_amalgam_key_eight_bit_count amalgam_key
332
+ amalgam_key_eight_bit_length = amalgam_key.to_s.bytesize / 8
333
+ amalgam_key_eight_bit_msg = "Expecting #{AMALGAM_KEY_EIGHT_BYTE_COUNT} eight bit blocks not #{amalgam_key_eight_bit_length}."
334
+ raise RuntimeError, amalgam_key_eight_bit_msg unless amalgam_key_eight_bit_length == AMALGAM_KEY_EIGHT_BYTE_COUNT
335
+ end
336
+
337
+
338
+ end
339
+
340
+
341
+ end