chef-encrypted-attributes 0.3.0 → 0.4.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +40 -4
- data/CONTRIBUTING.md +7 -6
- data/KNIFE.md +151 -0
- data/README.md +70 -192
- data/Rakefile +27 -14
- data/TESTING.md +18 -7
- data/TODO.md +2 -5
- data/lib/chef-encrypted-attributes.rb +7 -1
- data/lib/chef/encrypted_attribute.rb +282 -121
- data/lib/chef/encrypted_attribute/api.rb +521 -0
- data/lib/chef/encrypted_attribute/assertions.rb +16 -6
- data/lib/chef/encrypted_attribute/cache_lru.rb +54 -13
- data/lib/chef/encrypted_attribute/config.rb +198 -89
- data/lib/chef/encrypted_attribute/encrypted_mash.rb +127 -33
- data/lib/chef/encrypted_attribute/encrypted_mash/version0.rb +236 -48
- data/lib/chef/encrypted_attribute/encrypted_mash/version1.rb +249 -36
- data/lib/chef/encrypted_attribute/encrypted_mash/version2.rb +133 -19
- data/lib/chef/encrypted_attribute/exceptions.rb +19 -3
- data/lib/chef/encrypted_attribute/local_node.rb +15 -4
- data/lib/chef/encrypted_attribute/remote_clients.rb +33 -17
- data/lib/chef/encrypted_attribute/remote_node.rb +84 -29
- data/lib/chef/encrypted_attribute/remote_nodes.rb +62 -11
- data/lib/chef/encrypted_attribute/remote_users.rb +58 -19
- data/lib/chef/encrypted_attribute/search_helper.rb +214 -74
- data/lib/chef/encrypted_attribute/version.rb +3 -1
- data/lib/chef/encrypted_attributes.rb +20 -0
- data/lib/chef/knife/core/config.rb +4 -1
- data/lib/chef/knife/core/encrypted_attribute_base.rb +179 -0
- data/lib/chef/knife/core/encrypted_attribute_depends.rb +43 -0
- data/lib/chef/knife/core/encrypted_attribute_editor_options.rb +125 -61
- data/lib/chef/knife/encrypted_attribute_create.rb +51 -31
- data/lib/chef/knife/encrypted_attribute_delete.rb +32 -40
- data/lib/chef/knife/encrypted_attribute_edit.rb +51 -32
- data/lib/chef/knife/encrypted_attribute_show.rb +30 -55
- data/lib/chef/knife/encrypted_attribute_update.rb +43 -28
- data/spec/benchmark_helper.rb +2 -1
- data/spec/integration_helper.rb +1 -0
- data/spec/spec_helper.rb +21 -7
- metadata +75 -36
- metadata.gz.sig +1 -1
- data/API.md +0 -174
- data/INTERNAL.md +0 -166
@@ -1,3 +1,4 @@
|
|
1
|
+
# encoding: UTF-8
|
1
2
|
#
|
2
3
|
# Author:: Xabier de Zuazo (<xabier@onddo.com>)
|
3
4
|
# Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
|
@@ -21,61 +22,125 @@ require 'chef/encrypted_attribute/exceptions'
|
|
21
22
|
|
22
23
|
class Chef
|
23
24
|
class EncryptedAttribute
|
25
|
+
# Mash structure with embedded Mash structure encrypted.
|
26
|
+
#
|
27
|
+
# This is the most basic encrypted object, which inherits from `Chef::Mash`.
|
28
|
+
#
|
29
|
+
# This class is used to construct the different EncryptedAttribute versions.
|
30
|
+
# Each version implements the encryption in a different way or using
|
31
|
+
# different algorithms.
|
32
|
+
#
|
33
|
+
# Currently three {EncryptedMash} versions exists. But you can create your
|
34
|
+
# own versions and name it with the
|
35
|
+
# `Chef::EncryptedAttribute::EncryptedMash::Version` prefix.
|
36
|
+
#
|
37
|
+
# Uses {EncryptedMash::Version1} by default.
|
38
|
+
#
|
39
|
+
# This class is oriented to be easily integrable with chef in the future
|
40
|
+
# using `JSONCompat`.
|
41
|
+
#
|
42
|
+
# @see .create
|
43
|
+
# @see EncryptedMash::Version0
|
44
|
+
# @see EncryptedMash::Version1
|
45
|
+
# @see EncryptedMash::Version2
|
24
46
|
class EncryptedMash < Mash
|
25
|
-
|
26
|
-
#
|
27
|
-
# chef in the future using JSONCompat
|
28
|
-
|
47
|
+
# Mash key name to use for JSON class name. Chef uses the `'json_class'`
|
48
|
+
# key internally for objects, we use a renamed key.
|
29
49
|
JSON_CLASS = 'x_json_class'.freeze
|
50
|
+
|
51
|
+
# Mash key name to use for Chef object type.
|
30
52
|
CHEF_TYPE = 'chef_type'.freeze
|
31
|
-
CHEF_TYPE_VALUE = 'encrypted_attribute'.freeze
|
32
53
|
|
33
|
-
|
54
|
+
# Chef object type value.
|
55
|
+
CHEF_TYPE_VALUE = 'encrypted_attribute'.freeze
|
34
56
|
|
35
|
-
|
57
|
+
# Name prefix for all EncryptedAttribute version classes.
|
58
|
+
# Used internally by the #self.version_class method.
|
59
|
+
# @api private
|
60
|
+
VERSION_PREFIX = "#{name}::Version"
|
61
|
+
|
62
|
+
# Encrypted Mash constructor.
|
63
|
+
#
|
64
|
+
# @param enc_hs [Mash] encrypted Mash to clone.
|
65
|
+
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
|
66
|
+
# format is wrong or does not exist.
|
67
|
+
def initialize(enc_hs = nil)
|
36
68
|
super
|
37
69
|
self[JSON_CLASS] = self.class.name
|
38
70
|
self[CHEF_TYPE] = CHEF_TYPE_VALUE
|
39
|
-
update_from!(enc_hs) if enc_hs.
|
71
|
+
update_from!(enc_hs) if enc_hs.is_a?(Hash)
|
40
72
|
end
|
41
73
|
|
42
|
-
%w
|
74
|
+
%w(encrypt decrypt can_be_decrypted_by? needs_update?).each do |meth|
|
43
75
|
define_method(meth) do
|
44
|
-
|
76
|
+
fail NotImplementedError,
|
77
|
+
"#{self.class}##{__method__} method not implemented."
|
45
78
|
end
|
46
79
|
end
|
47
80
|
|
81
|
+
# Checks whether an encrypted Mash exists.
|
82
|
+
#
|
83
|
+
# @param enc_hs [Mash] Mash to check.
|
84
|
+
# @return [Boolean] returns `true` if an encrypted Mash exists.
|
48
85
|
def self.exist?(enc_hs)
|
49
|
-
enc_hs.
|
50
|
-
|
51
|
-
|
52
|
-
|
86
|
+
enc_hs.is_a?(Hash) &&
|
87
|
+
enc_hs.key?(JSON_CLASS) &&
|
88
|
+
enc_hs[JSON_CLASS] =~ /^#{Regexp.escape(Module.nesting[1].name)}/ &&
|
89
|
+
enc_hs.key?(CHEF_TYPE) && enc_hs[CHEF_TYPE] == CHEF_TYPE_VALUE
|
53
90
|
end
|
54
91
|
|
92
|
+
# Checks whether an encrypted Mash exists.
|
93
|
+
#
|
94
|
+
# @param args [Mash] {exist?} arguments.
|
95
|
+
# @return [Boolean] returns `true` if an encrypted Mash exists.
|
96
|
+
# @deprecated Use {exist?} instead.
|
55
97
|
def self.exists?(*args)
|
56
|
-
Chef::Log.warn(
|
98
|
+
Chef::Log.warn(
|
99
|
+
"#{name}.exists? is deprecated in favor of #{name}.exist?."
|
100
|
+
)
|
57
101
|
exist?(*args)
|
58
102
|
end
|
59
103
|
|
104
|
+
# Factory method to construct an encrypted Mash.
|
105
|
+
#
|
106
|
+
# @param version [String, Fixnum] EncryptedMash version to use.
|
107
|
+
# @raise [RequirementsFailure] if the specified encrypted attribute
|
108
|
+
# version cannot be used.
|
109
|
+
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
|
110
|
+
# format is wrong.
|
111
|
+
# @raise [UnsupportedEncryptedAttributeFormat] if encrypted attribute
|
112
|
+
# format is not supported or unknown.
|
60
113
|
def self.create(version)
|
61
114
|
klass = version_klass(version)
|
62
115
|
klass.send(:new)
|
63
116
|
end
|
64
117
|
|
65
|
-
#
|
118
|
+
# Serializes this object as a Hash.
|
119
|
+
#
|
120
|
+
# @param a [Hash] Ruby _#to_json_ call arguments.
|
121
|
+
# @return [String] JSON representation of the object.
|
66
122
|
def to_json(*a)
|
67
123
|
for_json.to_json(*a)
|
68
124
|
end
|
69
125
|
|
70
|
-
# Returns a Hash
|
126
|
+
# Returns the object as a Ruby Hash.
|
127
|
+
#
|
128
|
+
# @return [Hash] ruby Hash represtation of the object.
|
71
129
|
def for_json
|
72
130
|
to_hash
|
73
131
|
end
|
74
132
|
|
75
|
-
#
|
133
|
+
# Replaces the EncryptedMash content from a Mash.
|
134
|
+
#
|
135
|
+
# @param enc_hs [Mash] Mash to clone.
|
136
|
+
# @return [Mash] `self`.
|
137
|
+
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
|
138
|
+
# format is wrong or does not exist.
|
76
139
|
def update_from!(enc_hs)
|
77
140
|
unless self.class.exist?(enc_hs)
|
78
|
-
|
141
|
+
fail UnacceptableEncryptedAttributeFormat,
|
142
|
+
'Trying to construct invalid encrypted attribute. Maybe it is '\
|
143
|
+
'not encrypted?'
|
79
144
|
end
|
80
145
|
enc_hs = enc_hs.dup
|
81
146
|
enc_hs.delete(JSON_CLASS)
|
@@ -83,26 +148,41 @@ class Chef
|
|
83
148
|
update(enc_hs)
|
84
149
|
end
|
85
150
|
|
86
|
-
#
|
151
|
+
# Creates an *EncryptedMash::Version* object from a JSON Hash.
|
152
|
+
#
|
153
|
+
# Reads the EncryptedMash version to create from the {JSON_CLASS} key.
|
154
|
+
#
|
155
|
+
# @param enc_hs [Mash] Encrypted Mash as a Mash. As it is read from node
|
156
|
+
# attributes.
|
157
|
+
# @return [EncryptedMash] *EncryptedMash::Version* object.
|
158
|
+
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
|
159
|
+
# format is wrong.
|
160
|
+
# @raise [UnsupportedEncryptedAttributeFormat] if encrypted attribute
|
161
|
+
# format is not supported or unknown.
|
87
162
|
def self.json_create(enc_hs)
|
88
163
|
klass = string_to_klass(enc_hs[JSON_CLASS])
|
89
164
|
if klass.nil?
|
90
|
-
|
165
|
+
fail UnsupportedEncryptedAttributeFormat,
|
166
|
+
"Unknown chef-encrypted-attribute class #{enc_hs[JSON_CLASS]}"
|
91
167
|
end
|
92
168
|
klass.send(:new, enc_hs)
|
93
169
|
end
|
94
170
|
|
95
|
-
|
96
|
-
|
171
|
+
# Gets the class reference from its string representation.
|
172
|
+
#
|
173
|
+
# @param class_name [String] the class name as string.
|
174
|
+
# @return [Class] the class reference.
|
175
|
+
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
|
176
|
+
# class name is wrong.
|
177
|
+
# @api private
|
97
178
|
def self.string_to_klass(class_name)
|
98
|
-
unless class_name.
|
99
|
-
|
179
|
+
unless class_name.is_a?(String)
|
180
|
+
fail UnacceptableEncryptedAttributeFormat,
|
181
|
+
"Bad chef-encrypted-attribute class name #{class_name.inspect}"
|
100
182
|
end
|
101
183
|
begin
|
102
|
-
|
103
|
-
|
104
|
-
else
|
105
|
-
class_name.split('::').inject(Kernel) { |scope, const| scope.const_get(const, scope === Kernel) }
|
184
|
+
class_name.split('::').inject(Kernel) do |scope, const|
|
185
|
+
scope.const_get(const, scope == Kernel)
|
106
186
|
end
|
107
187
|
rescue NameError => e
|
108
188
|
Chef::Log.error(e)
|
@@ -110,18 +190,32 @@ class Chef
|
|
110
190
|
end
|
111
191
|
end
|
112
192
|
|
193
|
+
# Gets the class reference for a EncryptedMash version.
|
194
|
+
#
|
195
|
+
# The implementation of `"Chef::EncryptedAttribute::Version#{version}"`
|
196
|
+
# must exists and be included (`require`) beforehand.
|
197
|
+
#
|
198
|
+
# @param version [String, Fixnum] the EncryptedMash version.
|
199
|
+
# @return [Class] the EncryptedMash version class reference.
|
200
|
+
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
|
201
|
+
# version is wrong.
|
202
|
+
# @raise [UnsupportedEncryptedAttributeFormat] if encrypted attribute
|
203
|
+
# format is not supported or unknown.
|
204
|
+
# @api private
|
113
205
|
def self.version_klass(version)
|
114
|
-
version = version.to_s unless version.
|
206
|
+
version = version.to_s unless version.is_a?(String)
|
115
207
|
if version.empty?
|
116
|
-
|
208
|
+
fail UnacceptableEncryptedAttributeFormat,
|
209
|
+
"Bad chef-encrypted-attribute version #{version.inspect}"
|
117
210
|
end
|
118
211
|
klass = string_to_klass("#{VERSION_PREFIX}#{version}")
|
119
212
|
if klass.nil?
|
120
|
-
|
213
|
+
fail UnsupportedEncryptedAttributeFormat,
|
214
|
+
'This version of chef-encrypted-attribute does not support '\
|
215
|
+
"encrypted attribute item format version: \"#{version}\""
|
121
216
|
end
|
122
217
|
klass
|
123
218
|
end
|
124
|
-
|
125
219
|
end
|
126
220
|
end
|
127
221
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# encoding: UTF-8
|
1
2
|
#
|
2
3
|
# Author:: Xabier de Zuazo (<xabier@onddo.com>)
|
3
4
|
# Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
|
@@ -18,21 +19,67 @@
|
|
18
19
|
|
19
20
|
require 'chef/encrypted_attribute/encrypted_mash'
|
20
21
|
require 'chef/encrypted_attribute/exceptions'
|
21
|
-
require '
|
22
|
+
require 'ffi_yajl'
|
22
23
|
|
23
|
-
# Version0 format: using RSA without shared secret
|
24
24
|
class Chef
|
25
25
|
class EncryptedAttribute
|
26
26
|
class EncryptedMash
|
27
|
+
# EncryptedMash Version0 format: using RSA without shared secret.
|
28
|
+
#
|
29
|
+
# This is the first version, considered old. Uses public key cryptography
|
30
|
+
# (PKI) to encrypt the data. There is no shared secret or HMAC for data
|
31
|
+
# integrity checking.
|
32
|
+
#
|
33
|
+
# # `EncryptedMash::Version0` Structure
|
34
|
+
#
|
35
|
+
# If you try to read this encrypted attribute structure, you can see a
|
36
|
+
# `Chef::Mash` attribute with the following content:
|
37
|
+
#
|
38
|
+
# ```
|
39
|
+
# EncryptedMash
|
40
|
+
# └── encrypted_data
|
41
|
+
# ├── pub_key_hash1: The data encrypted using PKI for the public key 1
|
42
|
+
# │ (base64)
|
43
|
+
# ├── pub_key_hash2: The data encrypted using PKI for the public key 2
|
44
|
+
# │ (base64)
|
45
|
+
# └── ...
|
46
|
+
# ```
|
47
|
+
#
|
48
|
+
# The `public_key_hash1` key value is the *SHA1* of the public key used
|
49
|
+
# for encryption.
|
50
|
+
#
|
51
|
+
# Its content is the data encoded in *JSON*, then encrypted with the
|
52
|
+
# public key, and finally encoded in *base64*. The encryption is done
|
53
|
+
# using the *RSA* algorithm (PKI).
|
54
|
+
#
|
55
|
+
# @see EncryptedMash
|
27
56
|
class Version0 < Chef::EncryptedAttribute::EncryptedMash
|
28
|
-
|
57
|
+
# Encrypts data inside the current {EncryptedMash} object.
|
58
|
+
#
|
59
|
+
# @param value [Mixed] value to encrypt, will be converted to JSON.
|
60
|
+
# @param public_keys [Array<String, OpenSSL::PKey::RSA>] publics keys
|
61
|
+
# that will be able to decrypt the {EncryptedMash}.
|
62
|
+
# @return [EncryptedMash] the value encrypted.
|
63
|
+
# @raise [EncryptionFailure] if there are encryption errors.
|
64
|
+
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
|
65
|
+
# @raise [InvalidKey] if the RSA key format is wrong.
|
29
66
|
def encrypt(value, public_keys)
|
30
67
|
value_json = json_encode(value)
|
31
68
|
public_keys = parse_public_keys(public_keys)
|
32
|
-
self['encrypted_data'] =
|
69
|
+
self['encrypted_data'] =
|
70
|
+
rsa_encrypt_multi_key(value_json, public_keys)
|
33
71
|
self
|
34
72
|
end
|
35
73
|
|
74
|
+
# Decrypts the current {EncryptedMash} object.
|
75
|
+
#
|
76
|
+
# @param key [String, OpenSSL::PKey::RSA] RSA private key used to
|
77
|
+
# decrypt.
|
78
|
+
# @return [Mixed] the value decrypted.
|
79
|
+
# @raise [DecryptionFailure] if the data cannot be decrypted by the
|
80
|
+
# provided key.
|
81
|
+
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
|
82
|
+
# @raise [InvalidKey] if the RSA key format is wrong.
|
36
83
|
def decrypt(key)
|
37
84
|
key = parse_decryption_key(key)
|
38
85
|
value_json = rsa_decrypt_multi_key(self['encrypted_data'], key)
|
@@ -40,120 +87,261 @@ class Chef
|
|
40
87
|
# we avoid saving the decrypted value, only return it
|
41
88
|
end
|
42
89
|
|
90
|
+
# Checks if the current {EncryptedMash} can be decrypted by all of the
|
91
|
+
# provided keys.
|
92
|
+
#
|
93
|
+
# @param keys [Array<OpenSSL::PKey::RSA>] list of public keys.
|
94
|
+
# @return [Boolean] `true` if all keys can decrypt the data.
|
95
|
+
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
|
96
|
+
# @raise [InvalidKey] if the RSA key format is wrong.
|
43
97
|
def can_be_decrypted_by?(keys)
|
44
98
|
return false unless encrypted?
|
45
|
-
|
46
|
-
r and data_can_be_decrypted_by_key?(self['encrypted_data'], k)
|
47
|
-
end
|
99
|
+
data_can_be_decrypted_by_keys?(self['encrypted_data'], keys)
|
48
100
|
end
|
49
101
|
|
102
|
+
# Checks if the current {EncryptedMash} needs to be re-encrypted.
|
103
|
+
#
|
104
|
+
# This usually happends when new keys are provided or some keys are
|
105
|
+
# removed from the previous encryption process.
|
106
|
+
#
|
107
|
+
# In other words, this method checks all key can decrypt the data and
|
108
|
+
# only those keys.
|
109
|
+
#
|
110
|
+
# @param keys [Array<String, OpenSSL::PKey::RSA>] list of RSA public
|
111
|
+
# keys.
|
112
|
+
# @return [Boolean] `true` if all keys can decrypt the data and only
|
113
|
+
# those keys can decrypt the data.
|
114
|
+
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
|
115
|
+
# @raise [InvalidKey] if the RSA key format is wrong.
|
50
116
|
def needs_update?(keys)
|
51
117
|
keys = parse_public_keys(keys)
|
52
|
-
|
118
|
+
!can_be_decrypted_by?(keys) ||
|
119
|
+
self['encrypted_data'].keys.count != keys.count
|
53
120
|
end
|
54
121
|
|
55
122
|
protected
|
56
123
|
|
124
|
+
# Checks if encrypted data exists in the current Mash.
|
125
|
+
#
|
126
|
+
# @return [Boolean] `true` if there is encrypted data.
|
57
127
|
def encrypted?
|
58
|
-
|
128
|
+
key?('encrypted_data') && self['encrypted_data'].is_a?(Hash)
|
59
129
|
end
|
60
130
|
|
131
|
+
# Converts the RSA key to an `OpenSSL::PKey::RSA` object.
|
132
|
+
#
|
133
|
+
# @param k [String, OpenSSL::PKey::RSA] RSA key to convert.
|
134
|
+
# @return [OpenSSL::PKey::RSA] RSA key.
|
135
|
+
# @raise [InvalidKey] if the RSA key format is wrong.
|
61
136
|
def pem_to_key(k)
|
62
|
-
k.
|
63
|
-
rescue OpenSSL::PKey::RSAError, TypeError
|
64
|
-
raise
|
137
|
+
k.is_a?(OpenSSL::PKey::RSA) ? k : OpenSSL::PKey::RSA.new(k)
|
138
|
+
rescue OpenSSL::PKey::RSAError, TypeError
|
139
|
+
raise InvalidKey, "The provided key is invalid: #{k.inspect}"
|
65
140
|
end
|
66
141
|
|
142
|
+
# Parses a RSA public key used for encryption.
|
143
|
+
#
|
144
|
+
# @param key [String, OpenSSL::PKey::RSA] RSA key to parse.
|
145
|
+
# @return [OpenSSL::PKey::RSA] RSA public key.
|
146
|
+
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
|
147
|
+
# @raise [InvalidKey] if the RSA key format is wrong.
|
67
148
|
def parse_public_key(key)
|
68
149
|
key = pem_to_key(key)
|
69
150
|
unless key.public?
|
70
|
-
|
151
|
+
fail InvalidPublicKey, 'Invalid public key provided.'
|
71
152
|
end
|
72
153
|
key
|
73
154
|
end
|
74
155
|
|
156
|
+
# Parses a RSA key used for decryption. Must contain both the public
|
157
|
+
# and the private key. It also checks that the current {EncryptedMash}
|
158
|
+
# object can be decrypted by the provided key.
|
159
|
+
#
|
160
|
+
# @param key [String, OpenSSL::PKey::RSA] RSA key to parse.
|
161
|
+
# @return [OpenSSL::PKey::RSA] RSA key.
|
162
|
+
# @raise [DecryptionFailure] if the data cannot be decrypted by the
|
163
|
+
# provided key.
|
164
|
+
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
|
165
|
+
# @raise [InvalidKey] if the RSA key format is wrong.
|
75
166
|
def parse_decryption_key(key)
|
76
167
|
key = pem_to_key(key)
|
77
|
-
unless key.public?
|
78
|
-
|
168
|
+
unless key.public? && key.private?
|
169
|
+
fail InvalidKey,
|
170
|
+
'The provided key for decryption is invalid, a valid public '\
|
171
|
+
'and private key is required.'
|
79
172
|
end
|
80
|
-
|
81
|
-
|
173
|
+
# TODO: optimize, node key digest is calculated multiple times
|
174
|
+
unless can_be_decrypted_by?(key)
|
175
|
+
fail DecryptionFailure,
|
176
|
+
'Attribute data cannot be decrypted by the provided key.'
|
82
177
|
end
|
83
178
|
key
|
84
179
|
end
|
85
180
|
|
181
|
+
# Parses a list of RSA public keys, used for encryption.
|
182
|
+
#
|
183
|
+
# @param keys [Array<String, OpenSSL::PKey::RSA>] list of keys.
|
184
|
+
# @return [Array<OpenSSL::PKey::RSA>] list of keys parsed.
|
185
|
+
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
|
186
|
+
# @raise [InvalidKey] if the RSA key format is wrong.
|
86
187
|
def parse_public_keys(keys)
|
87
|
-
keys = [
|
88
|
-
keys.map
|
89
|
-
|
90
|
-
end.uniq { |k| k.public_key.to_s.chomp }
|
188
|
+
keys = [keys].flatten
|
189
|
+
keys_parsed = keys.map { |k| parse_public_key(k) }
|
190
|
+
keys_parsed.uniq { |k| k.public_key.to_s.chomp }
|
91
191
|
end
|
92
192
|
|
193
|
+
# Converts an object to its JSON representation.
|
194
|
+
#
|
195
|
+
# @param o [Mixed] object to convert.
|
196
|
+
# @return [String] JSON object as string.
|
93
197
|
def json_encode(o)
|
94
|
-
# TODO This does not check if the object is correct, should be an
|
95
|
-
|
198
|
+
# TODO: This does not check if the object is correct, should be an
|
199
|
+
# Array or a Hash
|
200
|
+
FFI_Yajl::Encoder.encode(o)
|
96
201
|
end
|
97
202
|
|
203
|
+
# Decodes a JSON string.
|
204
|
+
#
|
205
|
+
# @param o [String] JSON string to decode.
|
206
|
+
# @return [Mixed] Ruby representation of the JSON string.
|
207
|
+
# @raise [DecryptionFailure] if JSON string format is wrong.
|
98
208
|
def json_decode(o)
|
99
|
-
|
100
|
-
rescue
|
101
|
-
raise DecryptionFailure, "#{e.class.name}: #{e
|
209
|
+
FFI_Yajl::Parser.parse(o.to_s)
|
210
|
+
rescue FFI_Yajl::ParseError => e
|
211
|
+
raise DecryptionFailure, "#{e.class.name}: #{e}"
|
212
|
+
end
|
213
|
+
|
214
|
+
# Encodes Ruby `< 1.9.3` RSA key using X.509 format.
|
215
|
+
#
|
216
|
+
# In Ruby `< 1.9.3` RSA keys are in [PKCS#1]
|
217
|
+
# (http://en.wikipedia.org/wiki/PKCS_1) format.
|
218
|
+
#
|
219
|
+
# In Ruby `>= 1.9.3` RSA keys are in [X.509]
|
220
|
+
# (http://en.wikipedia.org/wiki/X.509) format (private keys in [PKCS#8]
|
221
|
+
# (http://en.wikipedia.org/wiki/PKCS_8)).
|
222
|
+
#
|
223
|
+
# @param rsa [OpenSSL::PKey::RSA] RSA key.
|
224
|
+
# @return [OpenSSL::ASN1::Sequence] RSA key in X.509 format.
|
225
|
+
# @note Heavily based on @sl4m code:
|
226
|
+
# https://gist.github.com/sl4m/1470360
|
227
|
+
def rsa_ensure_x509_ruby192(rsa)
|
228
|
+
modulus = rsa.n
|
229
|
+
exponent = rsa.e
|
230
|
+
|
231
|
+
asn1 = OpenSSL::ASN1
|
232
|
+
oid = asn1::ObjectId.new('rsaEncryption')
|
233
|
+
alg_id = asn1::Sequence.new([oid, asn1::Null.new(nil)])
|
234
|
+
ary = [asn1::Integer.new(modulus), asn1::Integer.new(exponent)]
|
235
|
+
pub_key = asn1::Sequence.new(ary)
|
236
|
+
enc_pk = asn1::BitString.new(pub_key.to_der)
|
237
|
+
asn1::Sequence.new([alg_id, enc_pk])
|
102
238
|
end
|
103
239
|
|
104
|
-
#
|
240
|
+
# Returns any RSA key in X.509 format.
|
241
|
+
#
|
242
|
+
# Fixes RSA key format in Ruby `< 1.9.3`.
|
243
|
+
#
|
244
|
+
# @param rsa [OpenSSL::PKey::RSA] RSA key.
|
245
|
+
# @return [OpenSSL::ASN1::Sequence] RSA key in X.509 format.
|
246
|
+
# @see #rsa_ensure_x509_ruby192
|
105
247
|
def rsa_ensure_x509(rsa)
|
106
|
-
|
107
|
-
modulus = rsa.n
|
108
|
-
exponent = rsa.e
|
109
|
-
|
110
|
-
oid = OpenSSL::ASN1::ObjectId.new('rsaEncryption')
|
111
|
-
alg_id = OpenSSL::ASN1::Sequence.new([oid, OpenSSL::ASN1::Null.new(nil)])
|
112
|
-
ary = [OpenSSL::ASN1::Integer.new(modulus), OpenSSL::ASN1::Integer.new(exponent)]
|
113
|
-
pub_key = OpenSSL::ASN1::Sequence.new(ary)
|
114
|
-
enc_pk = OpenSSL::ASN1::BitString.new(pub_key.to_der)
|
115
|
-
subject_pk_info = OpenSSL::ASN1::Sequence.new([alg_id, enc_pk])
|
116
|
-
else
|
117
|
-
rsa
|
118
|
-
end
|
248
|
+
RUBY_VERSION < '1.9.3' ? rsa_ensure_x509_ruby192(rsa) : rsa
|
119
249
|
end
|
120
250
|
|
251
|
+
# Gets the hash key to use for saving the encrypted data for a node.
|
252
|
+
#
|
253
|
+
# It uses a SHA1 hexadecimal digest of the public key as key.
|
254
|
+
#
|
255
|
+
# @param public_key [OpenSSL::PKey::RSA] RSA public key.
|
256
|
+
# @return [String] hash key for the public key.
|
121
257
|
def node_key(public_key)
|
122
258
|
Digest::SHA1.hexdigest(rsa_ensure_x509(public_key).to_der)
|
123
259
|
end
|
124
260
|
|
261
|
+
# Encrypts a value using a RSA public key.
|
262
|
+
#
|
263
|
+
# @param value [String] data to encrypt.
|
264
|
+
# @param public_key [OpenSSL::PKey::RSA] public key used for encryption.
|
265
|
+
# @return [String] data encrypted in its Base64 representation.
|
266
|
+
# @raise [EncryptionFailure] if there are encryption errors.
|
125
267
|
def rsa_encrypt_value(value, public_key)
|
126
268
|
Base64.encode64(public_key.public_encrypt(value))
|
127
269
|
rescue OpenSSL::PKey::RSAError => e
|
128
|
-
raise EncryptionFailure, "#{e.class.name}: #{e
|
270
|
+
raise EncryptionFailure, "#{e.class.name}: #{e}"
|
129
271
|
end
|
130
272
|
|
273
|
+
# Decrypts a value using a RSA private key.
|
274
|
+
#
|
275
|
+
# @param value [String] encrypted data to decrypt in its Base64
|
276
|
+
# representation.
|
277
|
+
# @param key [OpenSSL::PKey::RSA] private key used for decryption.
|
278
|
+
# @return [String] value decrypted.
|
279
|
+
# @raise [DecryptionFailure] if there are decryption errors.
|
131
280
|
def rsa_decrypt_value(value, key)
|
132
281
|
key.private_decrypt(Base64.decode64(value.to_s))
|
133
282
|
rescue OpenSSL::PKey::RSAError => e
|
134
|
-
raise DecryptionFailure, "#{e.class.name}: #{e
|
283
|
+
raise DecryptionFailure, "#{e.class.name}: #{e}"
|
135
284
|
end
|
136
285
|
|
286
|
+
# Returns data encrypted for multiple keys using RSA.
|
287
|
+
#
|
288
|
+
# Returns a `Mash` with the following structure:
|
289
|
+
# * Hash keys: hexadecimal SHA1 of the public key.
|
290
|
+
# * Hash values: RSA encrypted data and then converted to Base64.
|
291
|
+
#
|
292
|
+
# @param value [String] data to encrypt.
|
293
|
+
# @param public_keys [Array<OpenSSL::PKey::RSA>] public keys list.
|
294
|
+
# @return [Mash] data encrypted.
|
295
|
+
# @raise [EncryptionFailure] if there are encryption errors.
|
296
|
+
# @see #node_key
|
297
|
+
# @see #rsa_encrypt_value
|
137
298
|
def rsa_encrypt_multi_key(value, public_keys)
|
138
299
|
Mash.new(Hash[
|
139
300
|
public_keys.map do |public_key|
|
140
|
-
[
|
141
|
-
node_key(public_key),
|
142
|
-
rsa_encrypt_value(value, public_key),
|
143
|
-
]
|
301
|
+
[node_key(public_key), rsa_encrypt_value(value, public_key)]
|
144
302
|
end
|
145
|
-
|
303
|
+
])
|
146
304
|
end
|
147
305
|
|
306
|
+
# Decrypts RSA value from a data structure encrypted for multiple keys.
|
307
|
+
#
|
308
|
+
# @param enc_value [Mash] encrypted data structure.
|
309
|
+
# @param key [OpenSSL::PKey::RSA] RSA key to use (public and private key
|
310
|
+
# is required).
|
311
|
+
# @return [String] data decrypted.
|
312
|
+
# @see #rsa_decrypt_value
|
148
313
|
def rsa_decrypt_multi_key(enc_value, key)
|
149
314
|
enc_value = enc_value[node_key(key.public_key)]
|
150
315
|
rsa_decrypt_value(enc_value, key)
|
151
316
|
end
|
152
317
|
|
318
|
+
# Checks if data can be decrypted by the provided key. Where data is
|
319
|
+
# encrypted for multiple keys.
|
320
|
+
#
|
321
|
+
# This method is not immune to any kind of data corruption. Only checks
|
322
|
+
# that the data seems to be decipherable by the key. No MAC checking.
|
323
|
+
#
|
324
|
+
# @param enc_value [Mash] encrypted data structure.
|
325
|
+
# @param key [OpenSSL::PKey::RSA] RSA key.
|
326
|
+
# @return [Boolean] `true` if the data can be decrypted.
|
327
|
+
# @see #rsa_encrypt_multi_key
|
153
328
|
def data_can_be_decrypted_by_key?(enc_value, key)
|
154
|
-
enc_value.
|
329
|
+
enc_value.key?(node_key(key.public_key))
|
155
330
|
end
|
156
331
|
|
332
|
+
# Checks if the data can be decrypted by all of the provided keys.
|
333
|
+
#
|
334
|
+
# @param data [Mash] encrypted data to check. This usually refers to
|
335
|
+
# `self['encrypted_data']`.
|
336
|
+
# @param keys [Array<OpenSSL::PKey::RSA>] list of public keys.
|
337
|
+
# @return [Boolean] `true` if all keys can decrypt the data.
|
338
|
+
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
|
339
|
+
# @raise [InvalidKey] if the RSA key format is wrong.
|
340
|
+
def data_can_be_decrypted_by_keys?(data, keys)
|
341
|
+
parse_public_keys(keys).reduce(true) do |r, k|
|
342
|
+
r && data_can_be_decrypted_by_key?(data, k)
|
343
|
+
end
|
344
|
+
end
|
157
345
|
end
|
158
346
|
end
|
159
347
|
end
|