secure_data_bag 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,19 +13,19 @@ module SecureDataBag
13
13
  Chef::DataBag.validate_name!(bag.to_s)
14
14
  SecureDataBag::Item.validate_id!(item)
15
15
  SecureDataBag::Item.load(bag, item)
16
- rescue Exception
16
+ rescue StandardError
17
17
  Chef::Log.error("Failed to load secure data bag item: #{bag.inspect} #{item.inspect}")
18
18
  raise
19
19
  end
20
+
21
+ node.run_state[:secure_data_bag][bag][item] ||= data_bag_item if cache
22
+ data_bag_item
20
23
  end
21
24
 
22
- def secure_data_bag_item!(item, fields=[])
23
- secure = SecureDataBag::Item.from_item item
24
- secure.encoded_fields.concat(Array(fields))
25
+ def secure_data_bag_item!(item, metadata = {})
26
+ secure = SecureDataBag::Item.from_item(item, metadata)
25
27
  secure
26
28
  end
27
29
  end
28
30
  end
29
31
  end
30
-
31
-
@@ -0,0 +1,185 @@
1
+ require 'secure_data_bag/exceptions'
2
+
3
+ module SecureDataBag
4
+ module Encryptor
5
+ # Instantiate an Encryptor object responsable for encrypting the
6
+ # raw_hash with the secret.
7
+ #
8
+ # The optional metadata may contain hints as to how we should encrypt the
9
+ # raw_hash. Should hints not be provided, this will do it's best to
10
+ # detect the appropriate defaults.
11
+ #
12
+ # @param raw_hash [Hash] the raw hash to encrypt
13
+ # @param secret [String] the secret to encrypt with
14
+ # @param metadata [Hash] the optional metdata to configure the encryptor
15
+ # @return [SecureDataBag::NestedDecryptor] the object capable of decrypting
16
+ # @since 3.0.0
17
+ def self.new(raw_hash, secret, metadata = {})
18
+ metadata = Mash.new(metadata)
19
+ format = (metadata[:encryption_format] || metadata[:decryption_format])
20
+ case format
21
+ when 'encrypted'
22
+ SecureDataBag::FlatEncryptor.new(raw_hash, secret, metadata)
23
+ else
24
+ SecureDataBag::NestedEncryptor.new(raw_hash, secret, metadata)
25
+ end
26
+ end
27
+ end
28
+
29
+ # Encryptor object responsable for encrypting the raw_hash with the
30
+ # secret. This object is just a wrapper around
31
+ # Chef::EncryptedDataBagItem.
32
+ #
33
+ # @since 3.0.0
34
+ class FlatEncryptor
35
+ # The keys to encrypt
36
+ # @since 3.0.0
37
+ attr_reader :encrypted_keys
38
+
39
+ # The encrypted hash generated
40
+ # @since 3.0.0
41
+ attr_reader :encrypted_hash
42
+
43
+ # The decrypted hash to encrypt
44
+ # @since 3.0.0
45
+ attr_reader :decrypted_hash
46
+
47
+ # The metadata used to create the encrypted_hash
48
+ attr_reader :metadata
49
+
50
+ # Initializer
51
+ # @param decrypted_hash [Hash,String] the encrypted hash to encrypt
52
+ # @param secret [String] the secret to encrypt with
53
+ # @param metadata [Hash] optional metadata
54
+ # @since 3.0.0
55
+ def initialize(decrypted_hash, secret, metadata = {})
56
+ @secret = secret
57
+ @metadata = metadata
58
+ @encrypted_hash = {}
59
+ @encrypted_keys = []
60
+ @decrypted_hash = decrypted_hash
61
+ end
62
+
63
+ # Method called to encrpt the data structure and return it.
64
+ # @return [Hash] the encrypted value
65
+ # @since 3.0.0
66
+ def encrypt!
67
+ @encrypted_hash = encrypt
68
+ end
69
+
70
+ # Method called to encrpt the data structure and return it.
71
+ # @return [Hash] the encrypted value
72
+ # @since 3.0.0
73
+ def encrypt
74
+ ## NO WORKY
75
+ ## NO WORKY
76
+ ## NO WORKY
77
+ ## NO WORKY
78
+ ## NO WORKY
79
+ ## NO WORKY
80
+ Chef::EncryptedDataBagItem.encrypt_data_bag_item(
81
+ @decrypted_hash,
82
+ @secret
83
+ )
84
+ end
85
+
86
+ # Method name preserved for compatibility with
87
+ # Chef::EncryptedDataBagItem::Encryptor.
88
+ # @since 3.0.0
89
+ alias :for_encrypted_item :encrypt!
90
+ end
91
+
92
+ # Encryptor object responsable for encrypting the raw_hash with the
93
+ # secret. This object will recursively step through the raw_hash, looking for
94
+ # keys matching `encrypted_keys` and encrypt their values.
95
+ #
96
+ # @since 3.0.0
97
+ class NestedEncryptor
98
+ # The keys to encrypt
99
+ # @since 3.0.0
100
+ attr_reader :encrypted_keys
101
+
102
+ # The encrypted hash generated
103
+ # @since 3.0.0
104
+ attr_reader :encrypted_hash
105
+
106
+ # The decrypted hash to encrypt
107
+ # @since 3.0.0
108
+ attr_reader :decrypted_hash
109
+
110
+ # The metadata used to create the encrypted_hash
111
+ attr_reader :metadata
112
+
113
+ # Initializer
114
+ # @param decrypted_hash [Hash,String] the encrypted hash to encrypt
115
+ # @param secret [String] the secret to encrypt with
116
+ # @param metadata [Hash] optional metadata
117
+ # @since 3.0.0
118
+ def initialize(decrypted_hash, secret, metadata = {})
119
+ @secret = secret
120
+ @metadata = metadata
121
+ @encrypted_hash = {}
122
+ @encrypted_keys = case metadata[:encryption_format]
123
+ when 'plain' then @encrypted_keys = []
124
+ else metadata[:encrypted_keys] || []
125
+ end
126
+ @decrypted_hash = decrypted_hash
127
+ end
128
+
129
+ # Method called to encrpt the data structure and return it.
130
+ # @return [Hash] the encrypted value
131
+ # @since 3.0.0
132
+ def encrypt!
133
+ @encrypted_hash = encrypt
134
+ end
135
+
136
+ # Method called to encrpt the data structure and return it.
137
+ # @return [Hash] the encrypted value
138
+ # @since 3.0.0
139
+ def encrypt
140
+ encrypt_data(@decrypted_hash)
141
+ end
142
+
143
+ # Method name preserved for compatibility with
144
+ # Chef::EncryptedDataBagItem::Encryptor.
145
+ # @since 3.0.0
146
+ alias :for_encrypted_item :encrypt!
147
+
148
+ private
149
+
150
+ # Recursively encrypt hash values where keys match encryptable_key?
151
+ # @param raw_hash [Hash] the hash to encrypt
152
+ # @return [Hash] the encrypted hash
153
+ # @since 3.0.0
154
+ def encrypt_data(raw_hash)
155
+ encrypted_hash = Mash.new
156
+
157
+ raw_hash.each do |key, value|
158
+ value = if encryptable_key?(key)
159
+ encrypt_value(value)
160
+ elsif value.is_a?(Hash)
161
+ encrypt_data(value)
162
+ else value
163
+ end
164
+ encrypted_hash[key] = value
165
+ end
166
+
167
+ encrypted_hash
168
+ end
169
+
170
+ # Determine whether the hash key should be encrypted
171
+ # @return [Boolean]
172
+ # @since 3.0.0
173
+ def encryptable_key?(key)
174
+ @encrypted_keys.include?(key)
175
+ end
176
+
177
+ # Encrypt a single value
178
+ # @return [Hash] the encrypted value
179
+ # @since 3.0.0
180
+ def encrypt_value(value)
181
+ Chef::EncryptedDataBagItem::Encryptor
182
+ .new(value, @secret).for_encrypted_item
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,4 @@
1
+ module SecureDataBag
2
+ class UnsupportedSecureDataBagItemFormat < StandardError
3
+ end
4
+ end
@@ -1,192 +1,240 @@
1
-
2
- require 'open-uri'
3
1
  require 'chef/data_bag_item'
4
2
  require 'chef/encrypted_data_bag_item'
5
- require 'chef/encrypted_data_bag_item/encryptor'
6
- require 'chef/encrypted_data_bag_item/decryptor'
3
+ require 'secure_data_bag/constants'
4
+ require 'secure_data_bag/decryptor'
5
+ require 'secure_data_bag/encryptor'
7
6
 
8
7
  module SecureDataBag
9
- #
10
- # SecureDataBagItem extends the standard DataBagItem by providing it
11
- # with encryption / decryption capabilities.
12
- #
13
- # Although it does provide methods which may be used to specifically perform
14
- # crypto functions, it should be used the same way.
15
- #
16
-
17
8
  class Item < Chef::DataBagItem
18
- def initialize(opts={})
19
- # Chef 12.3 introduced the new option
20
- begin super(chef_server_rest: opts.delete(:chef_server_rest))
21
- rescue ArgumentError; super()
9
+ class << self
10
+ # Class method used to load the secret key from path
11
+ # @param path [String] the optional path to the file
12
+ # @return [String] the secret
13
+ # @since 3.0.0
14
+ def load_secret(path = nil)
15
+ path ||= (
16
+ Chef::Config[:knife][:secure_data_bag][:secret_file] ||
17
+ Chef::Config[:encrypted_data_bag_secret]
18
+ )
19
+ Chef::EncryptedDataBagItem.load_secret(path)
22
20
  end
23
21
 
24
- secret_path opts[:secret_path] if opts[:secret_path]
25
- secret opts[:secret] if opts[:secret]
26
- encoded_fields opts[:fields] if opts[:fields]
22
+ # Load a data_bag_item and convert into the a SecureDataBag::Item.
23
+ # @param data_bag [String] the data_bag to load the item from
24
+ # @param name [String] the data_bag_item id
25
+ # @param opts [Hash] optional options to pass to SecureDataBag::Item.new
26
+ # @return [SecureDataBag::Item]
27
+ # @since 3.0.0
28
+ def load(data_bag, name, opts = {})
29
+ data = {
30
+ 'data_bag' => data_bag,
31
+ 'id' => name
32
+ }.merge(
33
+ Chef::DataBagItem.load(data_bag, name).to_hash
34
+ )
35
+ item = from_hash(data, opts)
36
+ item
37
+ end
27
38
 
28
- self.raw_data = opts[:data] if opts[:data]
29
- self
30
- end
39
+ # Create a new SecureDataBag::Item from a hash and optional options.
40
+ # @param hash [Hash] the data
41
+ # @param opts [Hash] the optional options to pass to Item.new
42
+ # @return [SecureDataBag::Item]
43
+ # @since 3.0.0
44
+ def from_hash(hash, opts = {})
45
+ data = hash.dup
46
+ data.delete('chef_type')
47
+ data.delete('json_class')
48
+
49
+ metadata = Mash.new(data.delete(SecureDataBag::METADATA_KEY) || {})
50
+ metadata = metadata.merge(opts)
51
+
52
+ item = new(metadata)
53
+ item.data_bag(data.delete('data_bag')) if data.key?('data_bag')
54
+ item.raw_data = data.key?('raw_data') ? data['raw_data'] : data
55
+ item
56
+ end
31
57
 
32
- #
33
- # Path to encryption key file
34
- #
35
- def secret_path(arg=nil)
36
- set_or_return :secret_path, arg,
37
- kind_of: String,
38
- default: self.class.secret_path
58
+ # Create a new SecureDataBag::Item from a DataBagItem.
59
+ # @param data_bag_item [Chef::DataBagItem] the item to create from
60
+ # @param opts [Hash] the optional options ot pass to Item.new
61
+ # @return [SecureDataBag::Item]
62
+ # @since 3.0.0
63
+ def from_item(data_bag_item, opts = {})
64
+ data = data_bag_item.to_hash
65
+ from_hash(data, opts)
66
+ end
39
67
  end
40
68
 
41
- def self.secret_path(arg=nil)
42
- arg ||
43
- Chef::Config[:knife][:secure_data_bag][:secret_file] ||
44
- Chef::Config[:encrypted_data_bag_secret]
45
- end
69
+ # Initializer
70
+ # @param opts [Hash] optional options to configure the SecureDataBag::Item
71
+ # opts[:data] the initial data to set
72
+ # opts[:secret] the secret key to use when encrypting/decrypting
73
+ # opts[:secret_path] the path to the secret key
74
+ # opts[:encrypted_keys] an array of keys to encrypt
75
+ # opts[:format] the SecureDataBag::Item format to enforce
76
+ # @since 3.0.0
77
+ def initialize(opts = {})
78
+ opts = Mash.new(opts)
46
79
 
47
- #
48
- # Content of encryption secret
49
- #
50
- def secret(arg=nil)
51
- @secret = arg unless arg.nil?
52
- @secret ||= load_secret
53
- end
80
+ # Initiate the APIClient in Chef 12.3+
81
+ begin super(chef_server_rest: opts.delete(:chef_server_rest))
82
+ rescue ArgumentError; super()
83
+ end
54
84
 
55
- def load_secret
56
- @secret = self.class.load_secret(secret_path)
57
- end
85
+ # Optionally define the Item vesion
86
+ @version = opts[:version] || SecureDataBag::VERSION
58
87
 
59
- def self.load_secret(path=nil)
60
- Chef::EncryptedDataBagItem.load_secret(secret_path(path))
61
- end
88
+ # Optionally define the Item formats
89
+ @encryption_format = opts[:encryption_format]
90
+ @decryption_format = opts[:decryption_format]
62
91
 
63
- #
64
- # Fetch databag item via DataBagItem and then optionally decrypt
65
- #
66
- def self.load(data_bag, name, opts={})
67
- data = super(data_bag, name)
68
- new(opts.merge(data:data.to_hash))
69
- end
92
+ # Optionally provide the shared secret
93
+ @secret = opts[:secret] if opts[:secret]
70
94
 
71
- #
72
- # Setter for @raw_data
73
- # - ensure the data we receive is a Mash to support symbols
74
- # - pass it to DataBagItem for additional validation
75
- # - ensure the data has the encryption hash
76
- # - decode the data
77
- #
78
- def raw_data=(data)
79
- data = Mash.new(data)
80
- super(data)
81
- decode_data!
82
- end
95
+ # Optionally provide a path to the shared secret. If not provided, the
96
+ # secret loader will automatically attempt to select one.
97
+ @secret_path = opts[:secret_path]
83
98
 
84
- #
85
- # Fields we wish to encode
86
- #
87
- def encoded_fields(arg=nil)
88
- @encoded_fields = Array(arg).map{|s|s.to_s}.uniq if arg
89
- @encoded_fields ||= Chef::Config[:knife][:secure_data_bag][:fields] ||
90
- Array.new
91
- end
99
+ # Optionally provide a list of keys that should be encrypted or attempt
100
+ # to determine it based on configuration options.
101
+ @encrypted_keys = (
102
+ opts[:encrypted_keys] ||
103
+ Chef::Config[:knife][:secure_data_bag][:encrypted_keys] ||
104
+ []
105
+ ).uniq
92
106
 
93
- #
94
- # Raw Data decoder methods
95
- #
96
- def decode_data!
97
- @raw_data = decoded_data
98
- @raw_data
107
+ self.raw_data = opts[:data] if opts[:data]
108
+ self
99
109
  end
100
110
 
101
- def decoded_data
102
- if encoded_value?(@raw_data) then decode_value(@raw_data)
103
- else decode_hash(@raw_data)
104
- end
105
- end
111
+ # Array of hash keys which should be encrypted when encrypting this item.
112
+ # For previously decrypted items, this will contain the keys which has
113
+ # previously been encrypted.
114
+ # @since 3.0.0
115
+ attr_accessor :encrypted_keys
106
116
 
107
- def decode_hash(hash)
108
- hash.each do |k,v|
109
- v = if encoded_value?(v)
110
- encoded_fields(encoded_fields + [k])
111
- decode_value(v)
112
- elsif v.is_a?(Hash)
113
- decode_hash(v)
114
- else v
115
- end
116
- hash[k] = v
117
- end
118
- hash
119
- end
117
+ # Format to enforce when encrypting this Item. This item will automatically
118
+ # be updated when importing encrypted data.
119
+ # @since 3.0.0
120
+ attr_accessor :encryption_format
120
121
 
121
- def decode_value(value)
122
- Chef::EncryptedDataBagItem::Decryptor.
123
- for(value, secret).for_decrypted_item
124
- end
122
+ # Format to enforce when decrypting this Item. This item will automatically
123
+ # be updated when importing decrypted data.
124
+ # @since 3.0.0
125
+ attr_accessor :decryption_format
125
126
 
126
- def encoded_value?(value)
127
- value.is_a?(Hash) and value.key?(:encrypted_data)
127
+ # Fetch, Set or optionally Load the shared secret
128
+ # @param arg [String] optionally set the shared set
129
+ # @return [String] the shared secret
130
+ # @since 3.0.0
131
+ def secret(arg = nil)
132
+ @secret = arg unless arg.nil?
133
+ @secret ||= load_secret
128
134
  end
129
135
 
130
- #
131
- # Raw Data encoded methods
132
- #
133
- def encode_data!
134
- @raw_data = encoded_data
135
- @raw_data
136
+ # Hash representing the metadata associated to this Item
137
+ # @return [Hash] the metadata
138
+ # @since 3.0.0
139
+ def metadata
140
+ Mash.new(
141
+ encryption_format: @encryption_format,
142
+ decryption_format: @decryption_format,
143
+ encrypted_keys: @encrypted_keys,
144
+ version: @version
145
+ )
146
+ end
147
+
148
+ # Override the default setter to first ensure that the data is a Mash and
149
+ # then to automatically decrypt the data.
150
+ # @param new_data [Hash] the potentially encrypted data
151
+ # @since 3.0.0
152
+ def raw_data=(new_data)
153
+ new_data = Mash.new(new_data)
154
+ new_data.delete(SecureDataBag::METADATA_KEY)
155
+ super(decrypt_data!(new_data))
156
+ end
157
+
158
+ # Export this SecureDataBag::Item to it's raw_data
159
+ # @param opts [Hash] the optional options
160
+ # @return [Hash]
161
+ # @since 3.0.0
162
+ def to_data(opts = {})
163
+ opts = Mash.new(opts)
164
+ result = encrypt_data(raw_data)
165
+ result[SecureDataBag::METADATA_KEY] = metadata if opts[:metadata]
166
+ result
136
167
  end
137
168
 
138
- def encoded_data
139
- encode_hash(@raw_data.dup)
169
+ # Export this SecureDataBag::Item to a Chef::DataBagItem compatible hash
170
+ # @param opts [Hash] the optional options
171
+ # @return [Hash]
172
+ # @since 3.0.0
173
+ def to_hash(opts = {})
174
+ opts = Mash.new(opts)
175
+ result = to_data(opts)
176
+ result['chef_type'] = 'data_bag_item'
177
+ result['data_bag'] = data_bag.to_s
178
+ result
140
179
  end
141
180
 
142
- def encode_hash(hash)
143
- hash.each do |k,v|
144
- v = if encoded_fields.include?(k) then encode_value(v)
145
- elsif v.is_a?(Hash) then encode_hash(v)
146
- else v
147
- end
148
- hash[k] = v
149
- end
150
- hash
181
+ # Export this SecureDataBag::Item to a Chef::DataBagItem compatible json
182
+ # @return [String]
183
+ # @since 3.0.0
184
+ def to_json(*a)
185
+ result = {
186
+ 'name' => object_name,
187
+ 'json_class' => 'Chef::DataBagItem',
188
+ 'chef_type' => 'data_bag_item',
189
+ 'data_bag' => data_bag.to_s,
190
+ 'raw_data' => encrypt_data(raw_data)
191
+ }
192
+ result.to_json(*a)
151
193
  end
152
194
 
153
- def encode_value(value)
154
- Chef::EncryptedDataBagItem::Encryptor.
155
- new(value, secret).for_encrypted_item
195
+ private
196
+
197
+ # Load the shared secret from the configured secret_path (or auto-detect
198
+ # the path if undefined).
199
+ # @return [String] the shared secret
200
+ # @since 3.0.0
201
+ def load_secret
202
+ @secret = self.class.load_secret(@secret_path)
156
203
  end
157
204
 
158
- #
159
- # Transitions
160
- #
161
- def self.from_hash(h, opts={})
162
- item = new(opts.merge(data:h))
163
- item
205
+ # Decrypt the data, save the both the decrypted_keys and format for
206
+ # possible re-encryption, and return the descrypted hash.
207
+ # @param data [Hash] the potentially encrypted hash
208
+ # @param save [Boolean] whether to save the encrypted keys and format
209
+ # @return [Hash] the decrypted hash
210
+ # @since 3.0.0
211
+ def decrypt_data(data, save: false)
212
+ decryptor = SecureDataBag::Decryptor.for(data, secret, metadata)
213
+ decryptor.decrypt!
214
+ @encrypted_keys.concat(decryptor.decrypted_keys).uniq! if save
215
+ @decryption_format = decryptor.format if save
216
+ decryptor.decrypted_hash
164
217
  end
165
218
 
166
- def self.from_item(h, opts={})
167
- item = self.from_hash(h.to_hash, opts)
168
- item.data_bag h.data_bag
169
- item.encoded_fields h.encoded_fields if h.respond_to?(:encoded_fields)
170
- item
219
+ def decrypt_data!(data)
220
+ decrypt_data(data, save: true)
171
221
  end
172
222
 
173
- def to_hash(opts={})
174
- result = opts[:encoded] ? encoded_data : @raw_data
175
- result["chef_type"] = "data_bag_item"
176
- result["data_bag"] = data_bag
177
- result
223
+ # Encrypt the data and save the encrypted_keys.
224
+ # @param data [Hash] the hash to encrypt
225
+ # @param save [Boolean] whether to save the encrypted keys
226
+ # @return [Hash] the encrypted hash
227
+ # @since 3.0.0
228
+ def encrypt_data(data, _save: false)
229
+ encryptor = SecureDataBag::Encryptor.new(data, secret, metadata)
230
+ encryptor.encrypt!
231
+ encryptor.encrypted_hash
178
232
  end
179
233
 
180
- def to_json(*a)
181
- result = {
182
- name: self.object_name,
183
- json_class: "Chef::DataBagItem",
184
- chef_type: "data_bag_item",
185
- data_bag: data_bag,
186
- raw_data: encoded_data
187
- }
188
- result.to_json(*a)
234
+ def encrypt_data!(data)
235
+ encrypt_data(data, save: true)
189
236
  end
190
237
  end
191
238
  end
192
239
 
240
+ SecureDataBagItem = SecureDataBag::Item