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.
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,122 @@
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/mash'
20
+ require 'chef/encrypted_attribute/exceptions'
21
+
22
+ class Chef
23
+ class EncryptedAttribute
24
+ class EncryptedMash < Mash
25
+
26
+ # This class is oriented to be easily integrable with
27
+ # chef in the future using JSONCompat
28
+
29
+ JSON_CLASS = 'x_json_class'.freeze
30
+ CHEF_TYPE = 'chef_type'.freeze
31
+ CHEF_TYPE_VALUE = 'encrypted_attribute'.freeze
32
+
33
+ VERSION_PREFIX = "#{self.name}::Version"
34
+
35
+ def initialize(enc_hs=nil)
36
+ super
37
+ self[JSON_CLASS] = self.class.name
38
+ self[CHEF_TYPE] = CHEF_TYPE_VALUE
39
+ update_from!(enc_hs) if enc_hs.kind_of?(Hash)
40
+ end
41
+
42
+ %w{encrypt decrypt can_be_decrypted_by? needs_update?}.each do |meth|
43
+ define_method(meth) do
44
+ raise NotImplementedError, "#{self.class.to_s}##{__method__} method not implemented."
45
+ end
46
+ end
47
+
48
+ def self.exists?(enc_hs)
49
+ enc_hs.kind_of?(Hash) and
50
+ enc_hs.has_key?(JSON_CLASS) and
51
+ enc_hs[JSON_CLASS] =~ /^#{Regexp.escape(Module.nesting[1].name)}/ and
52
+ enc_hs.has_key?(CHEF_TYPE) and enc_hs[CHEF_TYPE] == CHEF_TYPE_VALUE
53
+ end
54
+
55
+ def self.create(version)
56
+ klass = version_klass(version)
57
+ klass.send(:new)
58
+ end
59
+
60
+ # Serialize this object as a Hash
61
+ def to_json(*a)
62
+ for_json.to_json(*a)
63
+ end
64
+
65
+ # Returns a Hash for JSON
66
+ def for_json
67
+ to_hash
68
+ end
69
+
70
+ # Update the EncryptedMash from Hash
71
+ def update_from!(enc_hs)
72
+ unless self.class.exists?(enc_hs)
73
+ raise UnacceptableEncryptedAttributeFormat, 'Trying to construct invalid encrypted attribute. Maybe it is not encrypted?'
74
+ end
75
+ enc_hs = enc_hs.dup
76
+ enc_hs.delete(JSON_CLASS)
77
+ enc_hs.delete(CHEF_TYPE)
78
+ update(enc_hs)
79
+ end
80
+
81
+ # Create an EncryptedMash::Version from JSON Hash
82
+ def self.json_create(enc_hs)
83
+ klass = string_to_klass(enc_hs[JSON_CLASS])
84
+ if klass.nil?
85
+ raise UnsupportedEncryptedAttributeFormat, "Unknown chef-encrypted-attribute class \"#{enc_hs[JSON_CLASS]}\""
86
+ end
87
+ klass.send(:new, enc_hs)
88
+ end
89
+
90
+ protected
91
+
92
+ def self.string_to_klass(class_name)
93
+ unless class_name.kind_of?(String)
94
+ raise UnacceptableEncryptedAttributeFormat, "Bad chef-encrypted-attribute class name \"#{class_name.inspect}\""
95
+ end
96
+ begin
97
+ if RUBY_VERSION < '1.9'
98
+ class_name.split('::').inject(Kernel) { |scope, const| scope.const_get(const) }
99
+ else
100
+ class_name.split('::').inject(Kernel) { |scope, const| scope.const_get(const, scope === Kernel) }
101
+ end
102
+ rescue NameError => e
103
+ Chef::Log.error(e)
104
+ nil
105
+ end
106
+ end
107
+
108
+ def self.version_klass(version)
109
+ version = version.to_s unless version.kind_of?(String)
110
+ if version.empty?
111
+ raise UnacceptableEncryptedAttributeFormat, "Bad chef-encrypted-attribute version \"#{version.inspect}\""
112
+ end
113
+ klass = string_to_klass("#{VERSION_PREFIX}#{version}")
114
+ if klass.nil?
115
+ raise UnsupportedEncryptedAttributeFormat, "This version of chef-encrypted-attribute does not support encrypted attribute item format version: \"#{version}\""
116
+ end
117
+ klass
118
+ end
119
+
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,143 @@
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/encrypted_mash'
20
+ require 'chef/encrypted_attribute/exceptions'
21
+ require 'yajl'
22
+
23
+ # Version0 format: using RSA without shared secret
24
+ class Chef
25
+ class EncryptedAttribute
26
+ class EncryptedMash
27
+ class Version0 < Chef::EncryptedAttribute::EncryptedMash
28
+
29
+ def encrypt(value, public_keys)
30
+ value_json = json_encode(value)
31
+ public_keys = parse_public_keys(public_keys)
32
+ self['encrypted_data'] = rsa_encrypt_multi_key(value_json, public_keys)
33
+ self
34
+ end
35
+
36
+ def decrypt(key)
37
+ key = parse_decryption_key(key)
38
+ value_json = rsa_decrypt_multi_key(self['encrypted_data'], key)
39
+ json_decode(value_json)
40
+ # we avoid saving the decrypted value, only return it
41
+ end
42
+
43
+ def can_be_decrypted_by?(keys)
44
+ return false unless encrypted?
45
+ parse_public_keys(keys).reduce(true) do |r, k|
46
+ r and data_can_be_decrypted_by_key?(self['encrypted_data'], k)
47
+ end
48
+ end
49
+
50
+ def needs_update?(keys)
51
+ keys = parse_public_keys(keys)
52
+ not can_be_decrypted_by?(keys) && self['encrypted_data'].keys.count == keys.count
53
+ end
54
+
55
+ protected
56
+
57
+ def encrypted?
58
+ has_key?('encrypted_data') and self['encrypted_data'].kind_of?(Hash)
59
+ end
60
+
61
+ def pem_to_key(k)
62
+ k.kind_of?(OpenSSL::PKey::RSA) ? k : OpenSSL::PKey::RSA.new(k)
63
+ rescue OpenSSL::PKey::RSAError, TypeError => e
64
+ raise InvalidPrivateKey, "The provided key is invalid: #{k.inspect}"
65
+ end
66
+
67
+ def parse_public_key(key)
68
+ key = pem_to_key(key)
69
+ unless key.public?
70
+ raise InvalidPublicKey, 'Invalid public key provided.'
71
+ end
72
+ key
73
+ end
74
+
75
+ def parse_decryption_key(key)
76
+ key = pem_to_key(key)
77
+ unless key.public? and key.private?
78
+ raise InvalidPrivateKey, 'The provided key for decryption is invalid, a valid public and private key is required.'
79
+ end
80
+ unless can_be_decrypted_by?(key) # TODO optimize, node key digest is calculated multiple times
81
+ raise DecryptionFailure, 'Attribute data cannot be decrypted by the provided key.'
82
+ end
83
+ key
84
+ end
85
+
86
+ def parse_public_keys(keys)
87
+ keys = [ keys ].flatten
88
+ keys.map do |k|
89
+ parse_public_key(k)
90
+ end.uniq { |k| k.public_key.to_s.chomp }
91
+ end
92
+
93
+ def json_encode(o)
94
+ # TODO This does not check if the object is correct, should be an Array or a Hash
95
+ Yajl::Encoder.encode(o)
96
+ end
97
+
98
+ def json_decode(o)
99
+ Yajl::Parser.parse(o.to_s)
100
+ rescue Yajl::ParseError => e
101
+ raise DecryptionFailure, "#{e.class.name}: #{e.to_s}"
102
+ end
103
+
104
+ def node_key(public_key)
105
+ Digest::SHA1.hexdigest(public_key.to_der)
106
+ end
107
+
108
+ def rsa_encrypt_value(value, public_key)
109
+ Base64.encode64(public_key.public_encrypt(value))
110
+ rescue OpenSSL::PKey::RSAError => e
111
+ raise EncryptionFailure, "#{e.class.name}: #{e.to_s}"
112
+ end
113
+
114
+ def rsa_decrypt_value(value, key)
115
+ key.private_decrypt(Base64.decode64(value.to_s))
116
+ rescue OpenSSL::PKey::RSAError => e
117
+ raise DecryptionFailure, "#{e.class.name}: #{e.to_s}"
118
+ end
119
+
120
+ def rsa_encrypt_multi_key(value, public_keys)
121
+ Mash.new(Hash[
122
+ public_keys.map do |public_key|
123
+ [
124
+ node_key(public_key),
125
+ rsa_encrypt_value(value, public_key),
126
+ ]
127
+ end
128
+ ])
129
+ end
130
+
131
+ def rsa_decrypt_multi_key(enc_value, key)
132
+ enc_value = enc_value[node_key(key.public_key)]
133
+ rsa_decrypt_value(enc_value, key)
134
+ end
135
+
136
+ def data_can_be_decrypted_by_key?(enc_value, key)
137
+ enc_value.has_key?(node_key(key.public_key))
138
+ end
139
+
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,140 @@
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/encrypted_mash/version0'
20
+ require 'chef/encrypted_attribute/exceptions'
21
+
22
+ # Version1 format: using RSA with a shared secret and message authentication (HMAC)
23
+ class Chef
24
+ class EncryptedAttribute
25
+ class EncryptedMash
26
+ class Version1 < Chef::EncryptedAttribute::EncryptedMash::Version0
27
+ SYMM_ALGORITHM = 'aes-256-cbc'
28
+ HMAC_ALGORITHM = 'sha256'
29
+
30
+ def encrypt(value, public_keys)
31
+ secrets = {}
32
+ value_json = json_encode(value)
33
+ public_keys = parse_public_keys(public_keys)
34
+ # encrypt the data
35
+ encrypted_data = symmetric_encrypt_value(value_json)
36
+ secrets['data'] = encrypted_data.delete('secret') # should no include the secret in clear
37
+ self['encrypted_data'] = encrypted_data
38
+ # generate hmac (encrypt-then-mac), excluding the secret
39
+ hmac = generate_hmac(json_encode(self['encrypted_data'].sort))
40
+ secrets['hmac'] = hmac.delete('secret')
41
+ self['hmac'] = hmac
42
+ # encrypt the shared secrets
43
+ self['encrypted_secret'] = rsa_encrypt_multi_key(json_encode(secrets), public_keys)
44
+ self
45
+ end
46
+
47
+ def decrypt(key)
48
+ key = parse_decryption_key(key)
49
+ enc_value = self['encrypted_data'].dup
50
+ hmac = self['hmac'].dup
51
+ # decrypt the shared secrets
52
+ secrets = json_decode(rsa_decrypt_multi_key(self['encrypted_secret'], key))
53
+ enc_value['secret'] = secrets['data']
54
+ hmac['secret'] = secrets['hmac']
55
+ # check hmac (encrypt-then-mac -> mac-then-decrypt)
56
+ unless hmac_matches?(hmac, json_encode(self['encrypted_data'].sort))
57
+ raise DecryptionFailure, 'Error decrypting encrypted attribute: invalid hmac. Most likely the data is corrupted.'
58
+ end
59
+ # decrypt the data
60
+ value_json = symmetric_decrypt_value(enc_value)
61
+ json_decode(value_json)
62
+ end
63
+
64
+ def can_be_decrypted_by?(keys)
65
+ return false unless encrypted?
66
+ parse_public_keys(keys).reduce(true) do |r, k|
67
+ r and data_can_be_decrypted_by_key?(self['encrypted_secret'], k)
68
+ end
69
+ end
70
+
71
+ def needs_update?(keys)
72
+ keys = parse_public_keys(keys)
73
+ not can_be_decrypted_by?(keys) && self['encrypted_secret'].keys.count == keys.count
74
+ end
75
+
76
+ protected
77
+
78
+ def encrypted?
79
+ super and
80
+ self['encrypted_data'].has_key?('iv') and
81
+ self['encrypted_data']['iv'].kind_of?(String) and
82
+ self['encrypted_data'].has_key?('data') and
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)
88
+ end
89
+
90
+ def symmetric_encrypt_value(value, algo=SYMM_ALGORITHM)
91
+ enc_value = Mash.new({ 'cipher' => algo })
92
+ begin
93
+ cipher = OpenSSL::Cipher.new(algo)
94
+ cipher.encrypt
95
+ enc_value['iv'] = Base64.encode64(cipher.iv = cipher.random_iv)
96
+ enc_value['secret'] = Base64.encode64(cipher.key = cipher.random_key)
97
+ enc_data = cipher.update(value) + cipher.final
98
+ rescue OpenSSL::Cipher::CipherError => e
99
+ raise EncryptionFailure, "#{e.class.name}: #{e.to_s}"
100
+ end
101
+ enc_value['data'] = Base64.encode64(enc_data)
102
+ enc_value
103
+ end
104
+
105
+ def symmetric_decrypt_value(enc_value, algo=SYMM_ALGORITHM)
106
+ cipher = OpenSSL::Cipher.new(enc_value['cipher'] || algo) # TODO maybe it's better to ignore [cipher] ?
107
+ cipher.decrypt
108
+ cipher.iv = Base64.decode64(enc_value['iv'])
109
+ cipher.key = Base64.decode64(enc_value['secret'])
110
+ cipher.update(Base64.decode64(enc_value['data'])) + cipher.final
111
+ rescue OpenSSL::Cipher::CipherError => e
112
+ raise DecryptionFailure, "#{e.class.name}: #{e.to_s}"
113
+ end
114
+
115
+ def generate_hmac(data, algo=HMAC_ALGORITHM)
116
+ hmac = Mash.new({ 'cipher' => algo }) # [cipher] is ignored, only as info
117
+ digest = OpenSSL::Digest.new(algo)
118
+ secret = OpenSSL::Random.random_bytes(digest.block_length)
119
+ hmac['secret'] = Base64.encode64(secret)
120
+ hmac['data'] = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data))
121
+ hmac
122
+ rescue OpenSSL::Digest::DigestError, OpenSSL::HMACError, RuntimeError => e
123
+ # RuntimeError is raised for unsupported algorithms
124
+ raise MessageAuthenticationFailure, "#{e.class.name}: #{e.to_s}"
125
+ end
126
+
127
+ def hmac_matches?(orig_hmac, data, algo=HMAC_ALGORITHM)
128
+ digest = OpenSSL::Digest.new(algo)
129
+ secret = Base64.decode64(orig_hmac['secret'])
130
+ new_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data))
131
+ orig_hmac['data'] == new_hmac
132
+ rescue OpenSSL::Digest::DigestError, OpenSSL::HMACError, RuntimeError => e
133
+ # RuntimeError is raised for unsupported algorithms
134
+ raise MessageAuthenticationFailure, "#{e.class.name}: #{e.to_s}"
135
+ end
136
+
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,38 @@
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
+ class Chef
20
+ class EncryptedAttribute
21
+
22
+ class UnsupportedEncryptedAttributeFormat < StandardError; end
23
+ class UnacceptableEncryptedAttributeFormat < StandardError; end
24
+ class DecryptionFailure < StandardError; end
25
+ class EncryptionFailure < StandardError; end
26
+ class MessageAuthenticationFailure < StandardError; end
27
+ class InvalidPublicKey < StandardError; end
28
+ class InvalidPrivateKey < StandardError; end
29
+
30
+ class InsufficientPrivileges < StandardError; end
31
+ class UserNotFound < StandardError; end
32
+
33
+ class SearchFailure < StandardError; end
34
+ class SearchFatalError < StandardError; end
35
+ class InvalidSearchKeys < StandardError; end
36
+
37
+ end
38
+ end
@@ -0,0 +1,38 @@
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
+ class Chef
20
+ class EncryptedAttribute
21
+ class LocalNode
22
+
23
+ # currently not used
24
+ def name
25
+ Chef::Config[:node_name]
26
+ end
27
+
28
+ def key
29
+ OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
30
+ end
31
+
32
+ def public_key
33
+ key.public_key
34
+ end
35
+
36
+ end
37
+ end
38
+ end