chef-encrypted-attributes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/API.md +163 -0
  3. data/CHANGELOG.md +7 -0
  4. data/INTERNAL.md +111 -0
  5. data/LICENSE +190 -0
  6. data/README.md +330 -0
  7. data/Rakefile +46 -0
  8. data/TESTING.md +45 -0
  9. data/TODO.md +20 -0
  10. data/lib/chef-encrypted-attributes.rb +19 -0
  11. data/lib/chef/encrypted_attribute.rb +218 -0
  12. data/lib/chef/encrypted_attribute/cache_lru.rb +74 -0
  13. data/lib/chef/encrypted_attribute/config.rb +200 -0
  14. data/lib/chef/encrypted_attribute/encrypted_mash.rb +122 -0
  15. data/lib/chef/encrypted_attribute/encrypted_mash/version0.rb +143 -0
  16. data/lib/chef/encrypted_attribute/encrypted_mash/version1.rb +140 -0
  17. data/lib/chef/encrypted_attribute/exceptions.rb +38 -0
  18. data/lib/chef/encrypted_attribute/local_node.rb +38 -0
  19. data/lib/chef/encrypted_attribute/remote_clients.rb +46 -0
  20. data/lib/chef/encrypted_attribute/remote_node.rb +111 -0
  21. data/lib/chef/encrypted_attribute/remote_users.rb +73 -0
  22. data/lib/chef/encrypted_attribute/search_helper.rb +144 -0
  23. data/lib/chef/encrypted_attribute/version.rb +23 -0
  24. data/lib/chef/knife/core/config.rb +19 -0
  25. data/lib/chef/knife/core/encrypted_attribute_editor_options.rb +100 -0
  26. data/lib/chef/knife/encrypted_attribute_create.rb +67 -0
  27. data/lib/chef/knife/encrypted_attribute_delete.rb +71 -0
  28. data/lib/chef/knife/encrypted_attribute_edit.rb +68 -0
  29. data/lib/chef/knife/encrypted_attribute_show.rb +86 -0
  30. data/lib/chef/knife/encrypted_attribute_update.rb +65 -0
  31. data/spec/benchmark_helper.rb +32 -0
  32. data/spec/integration_helper.rb +20 -0
  33. data/spec/spec_helper.rb +38 -0
  34. metadata +204 -0
@@ -0,0 +1,218 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/encrypted_attribute/config'
20
+ require 'chef/encrypted_attribute/encrypted_mash'
21
+ require 'chef/config'
22
+ require 'chef/mash'
23
+ require 'chef/api_client'
24
+
25
+ require 'chef/encrypted_attribute/local_node'
26
+ require 'chef/encrypted_attribute/remote_node'
27
+ require 'chef/encrypted_attribute/remote_clients'
28
+ require 'chef/encrypted_attribute/remote_users'
29
+ require 'chef/encrypted_attribute/encrypted_mash/version0'
30
+ require 'chef/encrypted_attribute/encrypted_mash/version1'
31
+
32
+ Chef::Config[:encrypted_attributes] = Mash.new unless Chef::Config[:encrypted_attributes].kind_of?(Hash)
33
+
34
+ class Chef
35
+ class EncryptedAttribute
36
+
37
+ def initialize(c=nil)
38
+ config(c)
39
+ end
40
+
41
+ def config(arg=nil)
42
+ @config ||= EncryptedAttribute::Config.new(Chef::Config[:encrypted_attributes])
43
+ @config.update!(arg) unless arg.nil?
44
+ @config
45
+ end
46
+
47
+ # Decrypts an encrypted attribute from a (encrypted) Hash
48
+ def load(enc_hs, key=nil)
49
+ enc_attr = EncryptedMash.json_create(enc_hs)
50
+ decrypted = enc_attr.decrypt(key || local_key)
51
+ decrypted['content'] # TODO check this Hash
52
+ end
53
+
54
+ # Decrypts a encrypted attribute from a remote node
55
+ def load_from_node(name, attr_ary, key=nil)
56
+ remote_node = RemoteNode.new(name)
57
+ self.load(remote_node.load_attribute(attr_ary, config.partial_search), key)
58
+ end
59
+
60
+ # Creates an encrypted attribute from a Hash
61
+ def create(value, keys=nil)
62
+ decrypted = { 'content' => value }
63
+
64
+ enc_attr = EncryptedMash.create(config.version)
65
+ enc_attr.encrypt(decrypted, target_keys(keys))
66
+ end
67
+
68
+ def create_on_node(name, attr_ary, value)
69
+ # read the client public key
70
+ node_public_key = Chef::ApiClient.load(name).public_key
71
+
72
+ # create the encrypted attribute
73
+ enc_attr = self.create(value, [ node_public_key ])
74
+
75
+ # save encrypted attribute
76
+ remote_node = RemoteNode.new(name)
77
+ remote_node.save_attribute(attr_ary, enc_attr)
78
+ end
79
+
80
+ # Updates the keys for which the attribute is encrypted
81
+ def update(enc_hs, keys=nil)
82
+ old_enc_attr = EncryptedMash.json_create(enc_hs)
83
+ if old_enc_attr.needs_update?(target_keys(keys))
84
+ hs = old_enc_attr.decrypt(local_key)
85
+ new_enc_attr = create(hs['content'], keys) # TODO check this Hash
86
+ enc_hs.replace(new_enc_attr)
87
+ true
88
+ else
89
+ false
90
+ end
91
+ end
92
+
93
+ def update_on_node(name, attr_ary)
94
+ # read the client public key
95
+ node_public_key = Chef::ApiClient.load(name).public_key
96
+
97
+ # update the encrypted attribute
98
+ remote_node = RemoteNode.new(name)
99
+ enc_hs = remote_node.load_attribute(attr_ary, config.partial_search)
100
+ updated = update(enc_hs, [ node_public_key ])
101
+
102
+ # save encrypted attribute
103
+ if updated
104
+ # TODO Node is accessed twice (here and RemoteNode#load_attribute above)
105
+ remote_node.save_attribute(attr_ary, enc_hs)
106
+ end
107
+ updated
108
+ end
109
+
110
+ protected
111
+
112
+ def remote_client_keys
113
+ RemoteClients.get_public_keys(config.client_search, config.partial_search)
114
+ end
115
+
116
+ def remote_user_keys
117
+ RemoteUsers.get_public_keys(config.users)
118
+ end
119
+
120
+ def target_keys(keys=nil)
121
+ target_keys = config.keys + remote_client_keys + remote_user_keys
122
+ target_keys += keys if keys.kind_of?(Array)
123
+ target_keys
124
+ end
125
+
126
+ def local_key
127
+ self.class.local_node.key
128
+ end
129
+
130
+ def self.local_node
131
+ LocalNode.new
132
+ end
133
+
134
+ def self.config(arg)
135
+ config = EncryptedAttribute::Config.new(Chef::Config[:encrypted_attributes])
136
+ config.update!(arg)
137
+ config.keys(config.keys + [ self.local_node.public_key ])
138
+ config
139
+ end
140
+
141
+ public
142
+
143
+ def self.load(hs, c={})
144
+ Chef::Log.debug("#{self.class.name}: Loading Local Encrypted Attribute from: #{hs.to_s}")
145
+ enc_attr = EncryptedAttribute.new(self.config(c))
146
+ result = enc_attr.load(hs)
147
+ Chef::Log.debug("#{self.class.name}: Local Encrypted Attribute loaded.")
148
+ result
149
+ end
150
+
151
+ def self.load_from_node(name, attr_ary, c={})
152
+ Chef::Log.debug("#{self.class.name}: Loading Remote Encrypted Attribute from #{name}: #{attr_ary.to_s}")
153
+ enc_attr = EncryptedAttribute.new(self.config(c))
154
+ result = enc_attr.load_from_node(name, attr_ary)
155
+ Chef::Log.debug("#{self.class.name}: Remote Encrypted Attribute loaded.")
156
+ result
157
+ end
158
+
159
+ def self.create(value, c={})
160
+ Chef::Log.debug("#{self.class.name}: Creating Encrypted Attribute.")
161
+ enc_attr = EncryptedAttribute.new(self.config(c))
162
+ result = enc_attr.create(value)
163
+ Chef::Log.debug("#{self.class.name}: Encrypted Attribute created.")
164
+ result
165
+ end
166
+
167
+ def self.create_on_node(name, attr_ary, value, c={})
168
+ Chef::Log.debug("#{self.class.name}: Creating Remote Encrypted Attribute on #{name}: #{attr_ary.to_s}")
169
+ enc_attr = EncryptedAttribute.new(self.config(c))
170
+ result = enc_attr.create_on_node(name, attr_ary, value)
171
+ Chef::Log.debug("#{self.class.name}: Encrypted Remote Attribute created.")
172
+ result
173
+ end
174
+
175
+ def self.update(hs, c={})
176
+ Chef::Log.debug("#{self.class.name}: Updating Encrypted Attribute: #{hs.to_s}")
177
+ enc_attr = EncryptedAttribute.new(self.config(c))
178
+ result = enc_attr.update(hs)
179
+ if result
180
+ Chef::Log.debug("#{self.class.name}: Encrypted Attribute updated.")
181
+ else
182
+ Chef::Log.debug("#{self.class.name}: Encrypted Attribute not updated.")
183
+ end
184
+ result
185
+ end
186
+
187
+ def self.update_on_node(name, attr_ary, c={})
188
+ Chef::Log.debug("#{self.class.name}: Updating Remote Encrypted Attribute on #{name}: #{attr_ary.to_s}")
189
+ enc_attr = EncryptedAttribute.new(self.config(c))
190
+ result = enc_attr.update_on_node(name, attr_ary)
191
+ if result
192
+ Chef::Log.debug("#{self.class.name}: Encrypted Remote Attribute updated.")
193
+ else
194
+ Chef::Log.debug("#{self.class.name}: Encrypted Remote Attribute not updated.")
195
+ end
196
+ result
197
+ end
198
+
199
+ def self.exists?(hs)
200
+ Chef::Log.debug("#{self.class.name}: Checking if Encrypted Attribute exists here: #{hs.to_s}")
201
+ result = EncryptedMash.exists?(hs)
202
+ if result
203
+ Chef::Log.debug("#{self.class.name}: Encrypted Attribute found.")
204
+ else
205
+ Chef::Log.debug("#{self.class.name}: Encrypted Attribute not found.")
206
+ end
207
+ result
208
+ end
209
+
210
+ def self.exists_on_node?(name, attr_ary, c={})
211
+ Chef::Log.debug("#{self.class.name}: Checking if Remote Encrypted Attribute exists on #{name}")
212
+ remote_node = RemoteNode.new(name)
213
+ node_attr = remote_node.load_attribute(attr_ary, self.config(c).partial_search)
214
+ Chef::EncryptedAttribute.exists?(node_attr)
215
+ end
216
+
217
+ end
218
+ end
@@ -0,0 +1,74 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/mixin/params_validate'
20
+
21
+ # Based on https://github.com/SamSaffron/lru_redux
22
+ class Chef
23
+ class EncryptedAttribute
24
+ class CacheLru < Hash
25
+ include ::Chef::Mixin::ParamsValidate
26
+
27
+ def initialize(size=nil)
28
+ super
29
+ max_size(size)
30
+ end
31
+
32
+ def max_size(arg=nil)
33
+ set_or_return(
34
+ :max_size,
35
+ arg,
36
+ :kind_of => [ Fixnum ],
37
+ :default => 1024,
38
+ :callbacks => begin
39
+ { 'should not be lower that zero' => lambda { |x| x >= 0 } }
40
+ end
41
+ )
42
+ pop_tail unless arg.nil?
43
+ @max_size
44
+ end
45
+
46
+ def [](key)
47
+ if has_key?(key)
48
+ val = super(key)
49
+ self[key] = val
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
55
+ def []=(key, val)
56
+ if max_size > 0 # unnecessary "if", small optimization?
57
+ delete(key)
58
+ super(key, val)
59
+ pop_tail
60
+ end
61
+ val
62
+ end
63
+
64
+ protected
65
+
66
+ def pop_tail
67
+ while size > max_size
68
+ delete(first[0])
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,200 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/mixin/params_validate'
20
+
21
+ class Chef
22
+ class EncryptedAttribute
23
+ class Config
24
+ include ::Chef::Mixin::ParamsValidate
25
+
26
+ OPTIONS = [
27
+ :version,
28
+ :partial_search,
29
+ :client_search,
30
+ :users,
31
+ :keys,
32
+ ].freeze
33
+
34
+ def initialize(config=nil)
35
+ update!(config) unless config.nil?
36
+ end
37
+
38
+ def version(arg=nil)
39
+ unless arg.nil? or not arg.kind_of?(String)
40
+ arg = Integer(arg) rescue arg
41
+ end
42
+ set_or_return(
43
+ :version,
44
+ arg,
45
+ :kind_of => [ Fixnum, String ],
46
+ :default => 1
47
+ )
48
+ end
49
+
50
+ def partial_search(arg=nil)
51
+ set_or_return(
52
+ :partial_search,
53
+ arg,
54
+ :kind_of => [ TrueClass, FalseClass ],
55
+ :default => true
56
+ )
57
+ end
58
+
59
+ def client_search(arg=nil)
60
+ unless arg.nil? or not arg.kind_of?(String)
61
+ arg = [ arg ]
62
+ end
63
+ set_or_return(
64
+ :client_search,
65
+ arg,
66
+ :kind_of => Array,
67
+ :default => [],
68
+ :callbacks => config_search_array_callbacks
69
+ )
70
+ end
71
+
72
+ def users(arg=nil)
73
+ set_or_return(
74
+ :users,
75
+ arg,
76
+ :kind_of => [ String, Array ],
77
+ :default => [],
78
+ :callbacks => config_users_arg_callbacks
79
+ )
80
+ end
81
+
82
+ def keys(arg=nil)
83
+ set_or_return(
84
+ :keys,
85
+ arg,
86
+ :kind_of => Array,
87
+ :default => [],
88
+ :callbacks => config_valid_keys_array_callbacks
89
+ )
90
+ end
91
+
92
+ def update!(config)
93
+ if config.kind_of?(self.class)
94
+ OPTIONS.each do |attr|
95
+ value = dup_object(config.send(attr))
96
+ self.instance_variable_set("@#{attr.to_s}", value)
97
+ end
98
+ elsif config.kind_of?(Hash)
99
+ config.each do |attr, value|
100
+ attr = attr.to_sym if attr.kind_of?(String)
101
+ if OPTIONS.include?(attr)
102
+ value = dup_object(value)
103
+ self.send(attr, value)
104
+ else
105
+ Chef::Log.warn("#{self.class.to_s}: configuration method not found: \"#{attr.to_s}\".")
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def [](key)
112
+ key = key.to_sym if key.kind_of?(String)
113
+ if OPTIONS.include?(key)
114
+ self.send(key)
115
+ end
116
+ end
117
+
118
+ def []=(key, value)
119
+ key = key.to_sym if key.kind_of?(String)
120
+ if OPTIONS.include?(key)
121
+ self.send(key, value)
122
+ end
123
+ end
124
+
125
+ protected
126
+
127
+ def dup_object(o)
128
+ o.dup
129
+ rescue TypeError
130
+ o
131
+ end
132
+
133
+ def config_valid_search_array?(s_ary)
134
+ s_ary.each do |s|
135
+ return false unless s.kind_of?(String)
136
+ end
137
+ true
138
+ end
139
+
140
+ def config_search_array_callbacks
141
+ {
142
+ 'should be a valid array of search patterns' => lambda do |cs|
143
+ config_valid_search_array?(cs)
144
+ end
145
+ }
146
+ end
147
+
148
+ def config_valid_user_arg?(users)
149
+ return users == '*' if users.kind_of?(String)
150
+ users.each do |u|
151
+ return false unless u.kind_of?(String) and u.match(/^[a-z0-9\-_]+$/)
152
+ end
153
+ true
154
+ end
155
+
156
+ def config_users_arg_callbacks
157
+ {
158
+ 'should be a valid array of search patterns' => lambda do |us|
159
+ config_valid_user_arg?(us)
160
+ end
161
+ }
162
+ end
163
+
164
+ def config_valid_key?(k)
165
+ rsa_k = case k
166
+ when OpenSSL::PKey::RSA
167
+ k
168
+ when String
169
+ begin
170
+ OpenSSL::PKey::RSA.new(k)
171
+ rescue OpenSSL::PKey::RSAError, TypeError
172
+ nil
173
+ end
174
+ else
175
+ nil
176
+ end
177
+ return false if rsa_k.nil?
178
+ rsa_k.public?
179
+ end
180
+
181
+ def config_valid_keys_array?(k_ary)
182
+ k_ary.each do |k|
183
+ unless config_valid_key?(k)
184
+ return false
185
+ end
186
+ end
187
+ true
188
+ end
189
+
190
+ def config_valid_keys_array_callbacks
191
+ {
192
+ 'should be a valid array of keys' => lambda do |keys|
193
+ config_valid_keys_array?(keys)
194
+ end
195
+ }
196
+ end
197
+
198
+ end
199
+ end
200
+ end