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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +40 -4
- data/CONTRIBUTING.md +7 -6
- data/KNIFE.md +151 -0
- data/README.md +70 -192
- data/Rakefile +27 -14
- data/TESTING.md +18 -7
- data/TODO.md +2 -5
- data/lib/chef-encrypted-attributes.rb +7 -1
- data/lib/chef/encrypted_attribute.rb +282 -121
- data/lib/chef/encrypted_attribute/api.rb +521 -0
- data/lib/chef/encrypted_attribute/assertions.rb +16 -6
- data/lib/chef/encrypted_attribute/cache_lru.rb +54 -13
- data/lib/chef/encrypted_attribute/config.rb +198 -89
- data/lib/chef/encrypted_attribute/encrypted_mash.rb +127 -33
- data/lib/chef/encrypted_attribute/encrypted_mash/version0.rb +236 -48
- data/lib/chef/encrypted_attribute/encrypted_mash/version1.rb +249 -36
- data/lib/chef/encrypted_attribute/encrypted_mash/version2.rb +133 -19
- data/lib/chef/encrypted_attribute/exceptions.rb +19 -3
- data/lib/chef/encrypted_attribute/local_node.rb +15 -4
- data/lib/chef/encrypted_attribute/remote_clients.rb +33 -17
- data/lib/chef/encrypted_attribute/remote_node.rb +84 -29
- data/lib/chef/encrypted_attribute/remote_nodes.rb +62 -11
- data/lib/chef/encrypted_attribute/remote_users.rb +58 -19
- data/lib/chef/encrypted_attribute/search_helper.rb +214 -74
- data/lib/chef/encrypted_attribute/version.rb +3 -1
- data/lib/chef/encrypted_attributes.rb +20 -0
- data/lib/chef/knife/core/config.rb +4 -1
- data/lib/chef/knife/core/encrypted_attribute_base.rb +179 -0
- data/lib/chef/knife/core/encrypted_attribute_depends.rb +43 -0
- data/lib/chef/knife/core/encrypted_attribute_editor_options.rb +125 -61
- data/lib/chef/knife/encrypted_attribute_create.rb +51 -31
- data/lib/chef/knife/encrypted_attribute_delete.rb +32 -40
- data/lib/chef/knife/encrypted_attribute_edit.rb +51 -32
- data/lib/chef/knife/encrypted_attribute_show.rb +30 -55
- data/lib/chef/knife/encrypted_attribute_update.rb +43 -28
- data/spec/benchmark_helper.rb +2 -1
- data/spec/integration_helper.rb +1 -0
- data/spec/spec_helper.rb +21 -7
- metadata +75 -36
- metadata.gz.sig +1 -1
- data/API.md +0 -174
- 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
|
-
|
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'] =
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
91
|
-
|
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'] =
|
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
|
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
|
-
|
106
|
-
|
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
|
291
|
+
raise DecryptionFailure, "#{e.class.name}: #{e}"
|
114
292
|
end
|
115
293
|
|
116
|
-
|
117
|
-
|
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'] =
|
321
|
+
hmac['data'] =
|
322
|
+
Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data))
|
122
323
|
hmac
|
123
|
-
rescue
|
124
|
-
|
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
|
-
|
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
|
134
|
-
|
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
|
-
|
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
|
-
|
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'] =
|
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
|
64
|
-
|
65
|
-
|
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
|
-
|
74
|
-
|
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
|
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
|
-
|
90
|
-
|
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
|
213
|
+
raise DecryptionFailure, "#{e.class.name}: #{e}"
|
99
214
|
end
|
100
|
-
|
101
215
|
end
|
102
216
|
end
|
103
217
|
end
|