secure_data_bag 2.2.0 → 3.0.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.
@@ -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