chef-encrypted-attributes 0.1.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 +7 -0
- data/API.md +163 -0
- data/CHANGELOG.md +7 -0
- data/INTERNAL.md +111 -0
- data/LICENSE +190 -0
- data/README.md +330 -0
- data/Rakefile +46 -0
- data/TESTING.md +45 -0
- data/TODO.md +20 -0
- data/lib/chef-encrypted-attributes.rb +19 -0
- data/lib/chef/encrypted_attribute.rb +218 -0
- data/lib/chef/encrypted_attribute/cache_lru.rb +74 -0
- data/lib/chef/encrypted_attribute/config.rb +200 -0
- data/lib/chef/encrypted_attribute/encrypted_mash.rb +122 -0
- data/lib/chef/encrypted_attribute/encrypted_mash/version0.rb +143 -0
- data/lib/chef/encrypted_attribute/encrypted_mash/version1.rb +140 -0
- data/lib/chef/encrypted_attribute/exceptions.rb +38 -0
- data/lib/chef/encrypted_attribute/local_node.rb +38 -0
- data/lib/chef/encrypted_attribute/remote_clients.rb +46 -0
- data/lib/chef/encrypted_attribute/remote_node.rb +111 -0
- data/lib/chef/encrypted_attribute/remote_users.rb +73 -0
- data/lib/chef/encrypted_attribute/search_helper.rb +144 -0
- data/lib/chef/encrypted_attribute/version.rb +23 -0
- data/lib/chef/knife/core/config.rb +19 -0
- data/lib/chef/knife/core/encrypted_attribute_editor_options.rb +100 -0
- data/lib/chef/knife/encrypted_attribute_create.rb +67 -0
- data/lib/chef/knife/encrypted_attribute_delete.rb +71 -0
- data/lib/chef/knife/encrypted_attribute_edit.rb +68 -0
- data/lib/chef/knife/encrypted_attribute_show.rb +86 -0
- data/lib/chef/knife/encrypted_attribute_update.rb +65 -0
- data/spec/benchmark_helper.rb +32 -0
- data/spec/integration_helper.rb +20 -0
- data/spec/spec_helper.rb +38 -0
- 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
|