chef-encrypted-attributes 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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