chef-encrypted-attributes 0.3.0 → 0.4.0

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.yardopts +8 -0
  5. data/CHANGELOG.md +40 -4
  6. data/CONTRIBUTING.md +7 -6
  7. data/KNIFE.md +151 -0
  8. data/README.md +70 -192
  9. data/Rakefile +27 -14
  10. data/TESTING.md +18 -7
  11. data/TODO.md +2 -5
  12. data/lib/chef-encrypted-attributes.rb +7 -1
  13. data/lib/chef/encrypted_attribute.rb +282 -121
  14. data/lib/chef/encrypted_attribute/api.rb +521 -0
  15. data/lib/chef/encrypted_attribute/assertions.rb +16 -6
  16. data/lib/chef/encrypted_attribute/cache_lru.rb +54 -13
  17. data/lib/chef/encrypted_attribute/config.rb +198 -89
  18. data/lib/chef/encrypted_attribute/encrypted_mash.rb +127 -33
  19. data/lib/chef/encrypted_attribute/encrypted_mash/version0.rb +236 -48
  20. data/lib/chef/encrypted_attribute/encrypted_mash/version1.rb +249 -36
  21. data/lib/chef/encrypted_attribute/encrypted_mash/version2.rb +133 -19
  22. data/lib/chef/encrypted_attribute/exceptions.rb +19 -3
  23. data/lib/chef/encrypted_attribute/local_node.rb +15 -4
  24. data/lib/chef/encrypted_attribute/remote_clients.rb +33 -17
  25. data/lib/chef/encrypted_attribute/remote_node.rb +84 -29
  26. data/lib/chef/encrypted_attribute/remote_nodes.rb +62 -11
  27. data/lib/chef/encrypted_attribute/remote_users.rb +58 -19
  28. data/lib/chef/encrypted_attribute/search_helper.rb +214 -74
  29. data/lib/chef/encrypted_attribute/version.rb +3 -1
  30. data/lib/chef/encrypted_attributes.rb +20 -0
  31. data/lib/chef/knife/core/config.rb +4 -1
  32. data/lib/chef/knife/core/encrypted_attribute_base.rb +179 -0
  33. data/lib/chef/knife/core/encrypted_attribute_depends.rb +43 -0
  34. data/lib/chef/knife/core/encrypted_attribute_editor_options.rb +125 -61
  35. data/lib/chef/knife/encrypted_attribute_create.rb +51 -31
  36. data/lib/chef/knife/encrypted_attribute_delete.rb +32 -40
  37. data/lib/chef/knife/encrypted_attribute_edit.rb +51 -32
  38. data/lib/chef/knife/encrypted_attribute_show.rb +30 -55
  39. data/lib/chef/knife/encrypted_attribute_update.rb +43 -28
  40. data/spec/benchmark_helper.rb +2 -1
  41. data/spec/integration_helper.rb +1 -0
  42. data/spec/spec_helper.rb +21 -7
  43. metadata +75 -36
  44. metadata.gz.sig +1 -1
  45. data/API.md +0 -174
  46. data/INTERNAL.md +0 -166
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  #
2
3
  # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
4
  # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
@@ -19,122 +20,334 @@
19
20
  require 'chef/encrypted_attribute/encrypted_mash/version0'
20
21
  require 'chef/encrypted_attribute/exceptions'
21
22
 
22
- # Version1 format: using RSA with a shared secret and message authentication (HMAC)
23
23
  class Chef
24
24
  class EncryptedAttribute
25
25
  class EncryptedMash
26
+ # EncryptedMash Version1 format: using RSA with a shared secret and
27
+ # message authentication (HMAC).
28
+ #
29
+ # This is the {EncryptedMash} version used by default. Uses public key
30
+ # cryptography (PKI) to encrypt a shared secret. Then this shared secret
31
+ # is used to encrypt the data.
32
+ #
33
+ # * This implementation can be improved, is not optimized either for
34
+ # performance or for space.
35
+ # * Every time the `EncryptedAttribute` is updated, all the shared secrets
36
+ # are regenerated.
37
+ #
38
+ # # `EncryptedMash::Version1` Structure
39
+ #
40
+ # If you try to read this encrypted attribute structure, you can see a
41
+ # `Mash` attribute with the following content:
42
+ #
43
+ # ```
44
+ # EncryptedMash
45
+ # ├── chef_type: "encrypted_attribute" (string).
46
+ # ├── x_json_class: The used `EncryptedMash` version class name (string).
47
+ # ├── encrypted_data
48
+ # │ ├── cipher: The used PKI algorithm, "aes-256-cbc" (string).
49
+ # │ ├── data: PKI encrypted data (base64).
50
+ # │ └── iv: Initialization vector (in base64).
51
+ # ├── encrypted_secret
52
+ # │ ├── pub_key_hash1: The shared secrets encrypted for the public key 1
53
+ # │ │ (base64).
54
+ # │ ├── pub_key_hash2: The shared secrets encrypted for the public key 2
55
+ # │ │ (base64).
56
+ # │ └── ...
57
+ # └── hmac
58
+ # ├── cipher: The used HMAC algorithm, currently ignored and always
59
+ # │ "sha256" (string).
60
+ # └── data: Hash-based message authentication code value (base64).
61
+ # ```
62
+ #
63
+ # * `x_json_class` field is used, with the `x_` prefix, to be easily
64
+ # integrated with Chef in the future.
65
+ #
66
+ # ## `EncryptedMash[encrypted_data][data]`
67
+ #
68
+ # The data inside `encrypted_data` is symmetrically encrypted using the
69
+ # secret shared key. The data is converted to *JSON* before the
70
+ # encryption, then encrypted and finally encoded in *base64*. By default,
71
+ # the `'aes-256-cbc'` algorithm is used for encryption.
72
+ #
73
+ # After decryption, the *JSON* has the following structure:
74
+ #
75
+ # ```
76
+ # └── encrypted_data
77
+ # └── data (symmetrically encrypted JSON in base64)
78
+ # └── content: attribute content as a Mash.
79
+ # ```
80
+ #
81
+ # * In the future, this structure may contain some metadata like default
82
+ # configuration values.
83
+ #
84
+ # ## `EncryptedMash[encrypted_secret][pub_key_hash1]`
85
+ #
86
+ # The `public_key_hash1` key value is the *SHA1* of the public key used
87
+ # for encryption.
88
+ #
89
+ # Its content is the encrypted shared secrets in *base64*. The encryption
90
+ # is done using the *RSA* algorithm (PKI).
91
+ #
92
+ # After decryption, you find the following structure in *JSON*:
93
+ #
94
+ # ```
95
+ # └── encrypted_secret
96
+ # └── pub_key_hash1 (PKI encrypted JSON in base64)
97
+ # ├── data: The shared secret used to encrypt the data (base64).
98
+ # └── hmac: The shared secret used for the HMAC calculation
99
+ # (base64).
100
+ # ```
101
+ #
102
+ # ## `EncryptedMash[hmac][data]`
103
+ #
104
+ # The HMAC data is in *base64*. The hashing algorithm used is `'sha256'`.
105
+ #
106
+ # The following data is used in a alphabetically sorted *JSON* to
107
+ # calculate the HMAC:
108
+ #
109
+ # ```
110
+ # Data to calculate the HMAC from
111
+ # ├── cipher: The algorithm used for `encrypted_data` encryption
112
+ # │ ("aes-256-cbc").
113
+ # ├── data: The `encrypted_data` data content after the encryption
114
+ # │ (encrypt-then-mac).
115
+ # └── iv: The initialization vector used to encrypt the encrypted_data.
116
+ # ```
117
+ #
118
+ # * All the data required for decryption is included in the HMAC (except
119
+ # the secret key, of course): `cipher`, `data` and `iv`.
120
+ # * The data used to calculate the HMAC is the encrypted data, not the
121
+ # clear text data (**Encrypt-then-MAC**).
122
+ # * The secret used to calculate the HMAC is not the same as the secret
123
+ # used to encrypt the data.
124
+ # * The secret used to calculate the HMAC is shared inside
125
+ # `encrypted_secret` field with the data secret.
126
+ #
127
+ # @see EncryptedMash
26
128
  class Version1 < Chef::EncryptedAttribute::EncryptedMash::Version0
129
+ # Symmetric algorithm to use by default.
27
130
  SYMM_ALGORITHM = 'aes-256-cbc'
131
+ # Algorithm used for HMAC calculation.
28
132
  HMAC_ALGORITHM = 'sha256'
29
133
 
134
+ # (see EncryptedMash::Version0#encrypt)
135
+ # @raise [MessageAuthenticationFailure] if HMAC calculation error.
30
136
  def encrypt(value, public_keys)
31
137
  secrets = {}
32
138
  value_json = json_encode(value)
33
139
  public_keys = parse_public_keys(public_keys)
34
140
  # encrypt the data
35
141
  encrypted_data = symmetric_encrypt_value(value_json)
36
- secrets['data'] = encrypted_data.delete('secret') # should no include the secret in clear
142
+ # should no include the secret in clear
143
+ secrets['data'] = encrypted_data.delete('secret')
37
144
  self['encrypted_data'] = encrypted_data
38
145
  # generate hmac (encrypt-then-mac), excluding the secret
39
146
  hmac = generate_hmac(json_encode(self['encrypted_data'].sort))
40
147
  secrets['hmac'] = hmac.delete('secret')
41
148
  self['hmac'] = hmac
42
149
  # encrypt the shared secrets
43
- self['encrypted_secret'] = rsa_encrypt_multi_key(json_encode(secrets), public_keys)
150
+ self['encrypted_secret'] =
151
+ rsa_encrypt_multi_key(json_encode(secrets), public_keys)
44
152
  self
45
153
  end
46
154
 
155
+ # (see EncryptedMash::Version0#decrypt)
156
+ # @raise [MessageAuthenticationFailure] if HMAC calculation error.
47
157
  def decrypt(key)
48
158
  key = parse_decryption_key(key)
49
159
  enc_value = self['encrypted_data'].dup
50
160
  hmac = self['hmac'].dup
51
161
  # decrypt the shared secrets
52
- secrets = json_decode(rsa_decrypt_multi_key(self['encrypted_secret'], key))
162
+ secrets =
163
+ json_decode(rsa_decrypt_multi_key(self['encrypted_secret'], key))
53
164
  enc_value['secret'] = secrets['data']
54
165
  hmac['secret'] = secrets['hmac']
55
166
  # check hmac (encrypt-then-mac -> mac-then-decrypt)
56
167
  unless hmac_matches?(hmac, json_encode(self['encrypted_data'].sort))
57
- raise DecryptionFailure, 'Error decrypting encrypted attribute: invalid hmac. Most likely the data is corrupted.'
168
+ fail DecryptionFailure,
169
+ 'Error decrypting encrypted attribute: invalid hmac. Most '\
170
+ 'likely the data is corrupted.'
58
171
  end
59
172
  # decrypt the data
60
173
  value_json = symmetric_decrypt_value(enc_value)
61
174
  json_decode(value_json)
62
175
  end
63
176
 
177
+ # (see EncryptedMash::Version0#can_be_decrypted_by?)
64
178
  def can_be_decrypted_by?(keys)
65
179
  return false unless encrypted?
66
- parse_public_keys(keys).reduce(true) do |r, k|
67
- r and data_can_be_decrypted_by_key?(self['encrypted_secret'], k)
68
- end
180
+ data_can_be_decrypted_by_keys?(self['encrypted_secret'], keys)
69
181
  end
70
182
 
183
+ # (see EncryptedMash::Version0#needs_update?)
71
184
  def needs_update?(keys)
72
185
  keys = parse_public_keys(keys)
73
- not can_be_decrypted_by?(keys) && self['encrypted_secret'].keys.count == keys.count
186
+ !can_be_decrypted_by?(keys) ||
187
+ self['encrypted_secret'].keys.count != keys.count
74
188
  end
75
189
 
76
190
  protected
77
191
 
192
+ # Checks if the encrypted data Mash contains all the fields an has the
193
+ # correct type.
194
+ #
195
+ # Checks the `self['encrypted_data']` structure.
196
+ #
197
+ # @param [Hash<Symbol, Class>] fields to check. For example:
198
+ # `{ iv: String, data: String }`.
199
+ # @return [Boolean] `true` if all the fields are exists.
200
+ def encrypted_data_contain_fields?(fields)
201
+ data = self['encrypted_data']
202
+ fields.reduce(true) do |r, (field, kind_of)|
203
+ r && data.key?(field) && data[field].is_a?(kind_of)
204
+ end
205
+ end
206
+
207
+ # Checks if the encrypted data structure is correct.
208
+ #
209
+ # Checks the `self['encrypted_data']` structure.
210
+ #
211
+ # @return [Boolean] `true` if it is correct.
212
+ def encrypted_data?
213
+ encrypted_data_contain_fields?(iv: String, data: String)
214
+ end
215
+
216
+ # Checks if the encrypted secrets structure is correct.
217
+ #
218
+ # Checks the `self['encrypted_secret']` structure.
219
+ #
220
+ # @return [Boolean] `true` if it is correct.
221
+ def encrypted_secret?
222
+ self['encrypted_secret'].is_a?(Hash)
223
+ end
224
+
225
+ # Checks if the HMAC structure is correct.
226
+ #
227
+ # Checks the `self['hmac']` structure.
228
+ #
229
+ # @return [Boolean] `true` if it is correct.
230
+ def encrypted_hmac?
231
+ self['hmac'].is_a?(Hash) &&
232
+ self['hmac'].key?('data') &&
233
+ self['hmac']['data'].is_a?(String)
234
+ end
235
+
236
+ # (see EncryptedMash::Version0#encrypted?)
78
237
  def encrypted?
79
- super and
80
- self['encrypted_data'].has_key?('iv') and
81
- self['encrypted_data']['iv'].kind_of?(String) and
82
- self['encrypted_data'].has_key?('data') and
83
- self['encrypted_data']['data'].kind_of?(String) and
84
- self['encrypted_secret'].kind_of?(Hash) and
85
- self['hmac'].kind_of?(Hash) and
86
- self['hmac'].has_key?('data') and
87
- self['hmac']['data'].kind_of?(String)
238
+ super &&
239
+ encrypted_data? &&
240
+ encrypted_secret? &&
241
+ encrypted_hmac?
88
242
  end
89
243
 
90
- def symmetric_encrypt_value(value, algo=SYMM_ALGORITHM)
91
- enc_value = Mash.new({ 'cipher' => algo })
244
+ # Encrypts a value using a symmetric cryptographic algorithm.
245
+ #
246
+ # Uses a randomly generated secret and IV.
247
+ #
248
+ # @param value [String] data to encrypt.
249
+ # @param algo [String] symmetric algorithm to use.
250
+ # @return [Mash] hash structure with symmetrically encrypted data:
251
+ # * `['cipher']`: algorithm used.
252
+ # * `['secret']`: random secret used for encryption in Base64.
253
+ # * `['iv']`: random initialization vector in Base64.
254
+ # * `['data']`: data encrypted and in Base64.
255
+ # @raise [EncryptionFailure] if encryption error.
256
+ def symmetric_encrypt_value(value, algo = SYMM_ALGORITHM)
257
+ enc_value = Mash.new('cipher' => algo)
92
258
  begin
93
259
  cipher = OpenSSL::Cipher.new(algo)
94
260
  cipher.encrypt
95
- enc_value['secret'] = Base64.encode64(cipher.key = cipher.random_key)
261
+ enc_value['secret'] =
262
+ Base64.encode64(cipher.key = cipher.random_key)
96
263
  enc_value['iv'] = Base64.encode64(cipher.iv = cipher.random_iv)
97
264
  enc_data = cipher.update(value) + cipher.final
98
265
  rescue OpenSSL::Cipher::CipherError => e
99
- raise EncryptionFailure, "#{e.class.name}: #{e.to_s}"
266
+ raise EncryptionFailure, "#{e.class.name}: #{e}"
100
267
  end
101
268
  enc_value['data'] = Base64.encode64(enc_data)
102
269
  enc_value
103
270
  end
104
271
 
105
- def symmetric_decrypt_value(enc_value, algo=SYMM_ALGORITHM)
106
- cipher = OpenSSL::Cipher.new(enc_value['cipher'] || algo) # TODO maybe it's better to ignore [cipher] ?
272
+ # Decrypts data using a symmetric cryptographic algorithm.
273
+ #
274
+ # @param enc_value [Mash] hash structure with encrypted data:
275
+ # * `['cipher']`: algorithm used.
276
+ # * `['secret']`: secret used for encryption in Base64.
277
+ # * `['iv']`: initialization vector in Base64.
278
+ # * `['data']`: data encrypted in Base64.
279
+ # @param algo [String] symmetric algorithm to use.
280
+ # @raise [DecryptionFailure] if decryption error.
281
+ # @see #symmetric_encrypt_value
282
+ def symmetric_decrypt_value(enc_value, algo = SYMM_ALGORITHM)
283
+ # TODO: maybe it's better to ignore [cipher] ?
284
+ cipher = OpenSSL::Cipher.new(enc_value['cipher'] || algo)
107
285
  cipher.decrypt
108
286
  # We must set key before iv: https://bugs.ruby-lang.org/issues/8221
109
287
  cipher.key = Base64.decode64(enc_value['secret'])
110
288
  cipher.iv = Base64.decode64(enc_value['iv'])
111
289
  cipher.update(Base64.decode64(enc_value['data'])) + cipher.final
112
290
  rescue OpenSSL::Cipher::CipherError => e
113
- raise DecryptionFailure, "#{e.class.name}: #{e.to_s}"
291
+ raise DecryptionFailure, "#{e.class.name}: #{e}"
114
292
  end
115
293
 
116
- def generate_hmac(data, algo=HMAC_ALGORITHM)
117
- hmac = Mash.new({ 'cipher' => algo }) # [cipher] is ignored, only as info
294
+ # Exception list raised by the HMAC calculation process.
295
+ #
296
+ # `RuntimeError` is raised for unsupported algorithms.
297
+ #
298
+ # @api private
299
+ HMAC_EXCEPTIONS =
300
+ [OpenSSL::Digest::DigestError, OpenSSL::HMACError, RuntimeError]
301
+
302
+ # Calculates [HMAC]
303
+ # (http://en.wikipedia.org/wiki/Hash-based_message_authentication_code)
304
+ # value for data.
305
+ #
306
+ # Uses a randomly generated secret for the HMAC calculation.
307
+ #
308
+ # @param data [String] data to calculate HMAC for.
309
+ # @param algo [String] HMAC algorithm to use.
310
+ # @return [Mash] hash structure with HMAC data:
311
+ # * `['cipher']`: algorithm used.
312
+ # * `['secret']` random secret used for HMAC calculation in Base64.
313
+ # * `['data']` HMAC value in Base64.
314
+ # @raise [MessageAuthenticationFailure] if HMAC calculation error.
315
+ def generate_hmac(data, algo = HMAC_ALGORITHM)
316
+ # [cipher] is ignored, only as info
317
+ hmac = Mash.new('cipher' => algo)
118
318
  digest = OpenSSL::Digest.new(algo)
119
319
  secret = OpenSSL::Random.random_bytes(digest.block_length)
120
320
  hmac['secret'] = Base64.encode64(secret)
121
- hmac['data'] = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data))
321
+ hmac['data'] =
322
+ Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data))
122
323
  hmac
123
- rescue OpenSSL::Digest::DigestError, OpenSSL::HMACError, RuntimeError => e
124
- # RuntimeError is raised for unsupported algorithms
125
- raise MessageAuthenticationFailure, "#{e.class.name}: #{e.to_s}"
324
+ rescue *HMAC_EXCEPTIONS => e
325
+ raise MessageAuthenticationFailure, "#{e.class}: #{e}"
126
326
  end
127
327
 
128
- def hmac_matches?(orig_hmac, data, algo=HMAC_ALGORITHM)
328
+ # Checks if the [HMAC]
329
+ # (http://en.wikipedia.org/wiki/Hash-based_message_authentication_code)
330
+ # matches
331
+ #
332
+ # Uses a randomly generated secret for the HMAC calculation.
333
+ #
334
+ # @param orig_hmac [Mash] hash structure with HMAC data:
335
+ # * `['cipher']`: algorithm used (this is ignored).
336
+ # * `['secret']` secret used for HMAC calculation in Base64.
337
+ # * `['data']` HMAC value in Base64.
338
+ # @param data [String] data to calculate HMAC for.
339
+ # @param algo [String] HMAC algorithm to use.
340
+ # @return [Boolean] `true` if HMAC value matches.
341
+ # @raise [MessageAuthenticationFailure] if HMAC calculation error.
342
+ # @see #generate_hmac
343
+ def hmac_matches?(orig_hmac, data, algo = HMAC_ALGORITHM)
129
344
  digest = OpenSSL::Digest.new(algo)
130
345
  secret = Base64.decode64(orig_hmac['secret'])
131
346
  new_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data))
132
347
  orig_hmac['data'] == new_hmac
133
- rescue OpenSSL::Digest::DigestError, OpenSSL::HMACError, RuntimeError => e
134
- # RuntimeError is raised for unsupported algorithms
135
- raise MessageAuthenticationFailure, "#{e.class.name}: #{e.to_s}"
348
+ rescue *HMAC_EXCEPTIONS => e
349
+ raise MessageAuthenticationFailure, "#{e.class}: #{e}"
136
350
  end
137
-
138
351
  end
139
352
  end
140
353
  end
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  #
2
3
  # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
4
  # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
@@ -21,37 +22,119 @@ require 'chef/encrypted_attribute/encrypted_mash/version1'
21
22
  require 'chef/encrypted_attribute/assertions'
22
23
  require 'chef/encrypted_attribute/exceptions'
23
24
 
24
- # Version2 format: using RSA with a shared secret and GCM
25
25
  class Chef
26
26
  class EncryptedAttribute
27
27
  class EncryptedMash
28
+ # EncryptedMash Version2 format: using RSA with a shared secret and GCM.
29
+ #
30
+ # Uses public key cryptography (PKI) to encrypt a shared secret. Then this
31
+ # shared secret is used to encrypt the data using [GCM]
32
+ # (http://en.wikipedia.org/wiki/Galois/Counter_Mode).
33
+ #
34
+ # * This protocol version is based on the [Chef 12 Encrypted Data Bags
35
+ # Version 3 implementation](https://github.com/opscode/chef/pull/1591).
36
+ # * To use it, the following **special requirements** must be met:
37
+ # Ruby `>= 2` and OpenSSL `>= 1.0.1`.
38
+ # * This implementation can be improved, is not optimized either for
39
+ # performance or for space.
40
+ # * Every time the `EncryptedAttribute` is updated, all the shared secrets
41
+ # are regenerated.
42
+ #
43
+ # # `EncryptedMash::Version2` Structure
44
+ #
45
+ # If you try to read this encrypted attribute structure, you can see a
46
+ # `Mash` attribute with the following content:
47
+ #
48
+ # ```
49
+ # EncryptedMash
50
+ # ├── chef_type: "encrypted_attribute" (string).
51
+ # ├── x_json_class: The used `EncryptedMash` version class name (string).
52
+ # ├── encrypted_data
53
+ # │ ├── cipher: The used PKI algorithm, "aes-256-gcm" (string).
54
+ # │ ├── data: PKI encrypted data (base64).
55
+ # │ ├── auth_tag: GCM authentication tag (base64).
56
+ # │ └── iv: Initialization vector (in base64).
57
+ # └── encrypted_secret
58
+ # ├── pub_key_hash1: The shared secret encrypted for the public key 1
59
+ # │ (base64).
60
+ # ├── pub_key_hash2: The shared secret encrypted for the public key 2
61
+ # │ (base64).
62
+ # └── ...
63
+ # ```
64
+ #
65
+ # * `x_json_class` field is used, with the `x_` prefix, to be easily
66
+ # integrated with Chef in the future.
67
+ #
68
+ # ## `EncryptedMash[encrypted_data][data]`
69
+ #
70
+ # The data inside `encrypted_data` is symmetrically encrypted using the
71
+ # secret shared key. The data is converted to *JSON* before the
72
+ # encryption, then encrypted and finally encoded in *base64*. By default,
73
+ # the `'aes-256-gcm'` algorithm is used for encryption.
74
+ #
75
+ # After decryption, the *JSON* has the following structure:
76
+ #
77
+ # ```
78
+ # └── encrypted_data
79
+ # └── data (symmetrically encrypted JSON in base64)
80
+ # └── content: attribute content as a Mash.
81
+ # ```
82
+ #
83
+ # * In the future, this structure may contain some metadata like default
84
+ # configuration values.
85
+ #
86
+ # ## `EncryptedMash[encrypted_secret][pub_key_hash1]`
87
+ #
88
+ # The `public_key_hash1` key value is the *SHA1* of the public key used
89
+ # for encryption.
90
+ #
91
+ # Its content is the encrypted shared secret in *raw*. The encryption is
92
+ # done using the *RSA* algorithm (PKI).
93
+ #
94
+ # After decryption, you find the shared secret in *raw* (in *Version1*
95
+ # this is a *JSON* in *base64*).
96
+ #
97
+ # @see EncryptedMash
28
98
  class Version2 < Chef::EncryptedAttribute::EncryptedMash::Version1
29
99
  include Chef::EncryptedAttribute::Assertions
30
100
 
101
+ # Symmetric [AEAD]
102
+ # (http://en.wikipedia.org/wiki/AEAD_block_cipher_modes_of_operation)
103
+ # algorithm to use by default.
31
104
  ALGORITHM = 'aes-256-gcm'
32
105
 
33
- def initialize(enc_hs=nil)
106
+ # EncrytpedMash::Version2 constructor.
107
+ #
108
+ # Checks that GCM is correctly supported by Ruby and OpenSSL.
109
+ #
110
+ # @raise [RequirementsFailure] if the specified encrypted attribute
111
+ # version cannot be used.
112
+ def initialize(enc_hs = nil)
34
113
  assert_aead_requirements_met!(ALGORITHM)
35
114
  super
36
115
  end
37
116
 
117
+ # (see EncryptedMash::Version1#encrypt)
38
118
  def encrypt(value, public_keys)
39
119
  value_json = json_encode(value)
40
120
  public_keys = parse_public_keys(public_keys)
41
121
  # encrypt the data
42
122
  encrypted_data = symmetric_encrypt_value(value_json)
43
- secret = encrypted_data.delete('secret') # should no include the secret in clear
123
+ # should no include the secret in clear
124
+ secret = encrypted_data.delete('secret')
44
125
  self['encrypted_data'] = encrypted_data
45
126
  # encrypt the shared secret
46
127
  self['encrypted_secret'] = rsa_encrypt_multi_key(secret, public_keys)
47
128
  self
48
129
  end
49
130
 
131
+ # (see EncryptedMash::Version1#decrypt)
50
132
  def decrypt(key)
51
133
  key = parse_decryption_key(key)
52
134
  enc_value = self['encrypted_data'].dup
53
135
  # decrypt the shared secret
54
- enc_value['secret'] = rsa_decrypt_multi_key(self['encrypted_secret'], key)
136
+ enc_value['secret'] =
137
+ rsa_decrypt_multi_key(self['encrypted_secret'], key)
55
138
  # decrypt the data
56
139
  value_json = symmetric_decrypt_value(enc_value)
57
140
  json_decode(value_json)
@@ -59,19 +142,37 @@ class Chef
59
142
 
60
143
  protected
61
144
 
145
+ # (see EncryptedMash::Version1#encrypted_data?)
146
+ def encrypted_data?
147
+ encrypted_data_contain_fields?(
148
+ iv: String, auth_tag: String, data: String
149
+ )
150
+ end
151
+
152
+ # (see EncryptedMash::Version1#encrypted?)
62
153
  def encrypted?
63
- Version0.instance_method(:encrypted?).bind(self).call and
64
- self['encrypted_data'].has_key?('iv') and
65
- self['encrypted_data']['iv'].kind_of?(String) and
66
- self['encrypted_data'].has_key?('auth_tag') and
67
- self['encrypted_data']['auth_tag'].kind_of?(String) and
68
- self['encrypted_data'].has_key?('data') and
69
- self['encrypted_data']['data'].kind_of?(String) and
70
- self['encrypted_secret'].kind_of?(Hash)
154
+ Version0.instance_method(:encrypted?).bind(self).call &&
155
+ encrypted_data? &&
156
+ encrypted_secret?
71
157
  end
72
158
 
73
- def symmetric_encrypt_value(value, algo=ALGORITHM)
74
- enc_value = Mash.new({ 'cipher' => algo })
159
+ # Encrypts a value using a symmetric [AEAD]
160
+ # (http://en.wikipedia.org/wiki/AEAD_block_cipher_modes_of_operation)
161
+ # cryptographic algorithm.
162
+ #
163
+ # Uses a randomly generated secret and IV.
164
+ #
165
+ # @param value [String] data to encrypt.
166
+ # @param algo [String] symmetric algorithm to use.
167
+ # @return [Mash] hash structure with symmetrically encrypted data:
168
+ # * `['cipher']`: algorithm used.
169
+ # * `['secret']`: random secret used for encryption in Base64.
170
+ # * `['iv']`: random initialization vector in Base64.
171
+ # * `['auth_tag']`: authentication tag in Base64.
172
+ # * `['data']`: data encrypted and in Base64.
173
+ # @raise [EncryptionFailure] if encryption error.
174
+ def symmetric_encrypt_value(value, algo = ALGORITHM)
175
+ enc_value = Mash.new('cipher' => algo)
75
176
  begin
76
177
  cipher = OpenSSL::Cipher.new(algo)
77
178
  cipher.encrypt
@@ -80,14 +181,28 @@ class Chef
80
181
  enc_data = cipher.update(value) + cipher.final
81
182
  enc_value['auth_tag'] = Base64.encode64(cipher.auth_tag)
82
183
  rescue OpenSSL::Cipher::CipherError => e
83
- raise EncryptionFailure, "#{e.class.name}: #{e.to_s}"
184
+ raise EncryptionFailure, "#{e.class.name}: #{e}"
84
185
  end
85
186
  enc_value['data'] = Base64.encode64(enc_data)
86
187
  enc_value
87
188
  end
88
189
 
89
- def symmetric_decrypt_value(enc_value, algo=ALGORITHM)
90
- cipher = OpenSSL::Cipher.new(enc_value['cipher'] || algo) # TODO maybe it's better to ignore [cipher] ?
190
+ # Decrypts data using a symmetric [AEAD]
191
+ # (http://en.wikipedia.org/wiki/AEAD_block_cipher_modes_of_operation)
192
+ # cryptographic algorithm.
193
+ #
194
+ # @param enc_value [Mash] hash structure with encrypted data:
195
+ # * `['cipher']`: algorithm used.
196
+ # * `['secret']`: secret used for encryption in Base64.
197
+ # * `['iv']`: initialization vector in Base64.
198
+ # * `['auth_tag']`: authentication tag in Base64.
199
+ # * `['data']`: data encrypted in Base64.
200
+ # @param algo [String] symmetric algorithm to use.
201
+ # @raise [DecryptionFailure] if decryption error.
202
+ # @see #symmetric_encrypt_value
203
+ def symmetric_decrypt_value(enc_value, algo = ALGORITHM)
204
+ # TODO: maybe it's better to ignore [cipher] ?
205
+ cipher = OpenSSL::Cipher.new(enc_value['cipher'] || algo)
91
206
  cipher.decrypt
92
207
  # We must set key before iv: https://bugs.ruby-lang.org/issues/8221
93
208
  cipher.key = enc_value['secret']
@@ -95,9 +210,8 @@ class Chef
95
210
  cipher.auth_tag = Base64.decode64(enc_value['auth_tag'])
96
211
  cipher.update(Base64.decode64(enc_value['data'])) + cipher.final
97
212
  rescue OpenSSL::Cipher::CipherError => e
98
- raise DecryptionFailure, "#{e.class.name}: #{e.to_s}"
213
+ raise DecryptionFailure, "#{e.class.name}: #{e}"
99
214
  end
100
-
101
215
  end
102
216
  end
103
217
  end