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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.yardopts +8 -0
  5. data/CHANGELOG.md +40 -4
  6. data/CONTRIBUTING.md +7 -6
  7. data/KNIFE.md +151 -0
  8. data/README.md +70 -192
  9. data/Rakefile +27 -14
  10. data/TESTING.md +18 -7
  11. data/TODO.md +2 -5
  12. data/lib/chef-encrypted-attributes.rb +7 -1
  13. data/lib/chef/encrypted_attribute.rb +282 -121
  14. data/lib/chef/encrypted_attribute/api.rb +521 -0
  15. data/lib/chef/encrypted_attribute/assertions.rb +16 -6
  16. data/lib/chef/encrypted_attribute/cache_lru.rb +54 -13
  17. data/lib/chef/encrypted_attribute/config.rb +198 -89
  18. data/lib/chef/encrypted_attribute/encrypted_mash.rb +127 -33
  19. data/lib/chef/encrypted_attribute/encrypted_mash/version0.rb +236 -48
  20. data/lib/chef/encrypted_attribute/encrypted_mash/version1.rb +249 -36
  21. data/lib/chef/encrypted_attribute/encrypted_mash/version2.rb +133 -19
  22. data/lib/chef/encrypted_attribute/exceptions.rb +19 -3
  23. data/lib/chef/encrypted_attribute/local_node.rb +15 -4
  24. data/lib/chef/encrypted_attribute/remote_clients.rb +33 -17
  25. data/lib/chef/encrypted_attribute/remote_node.rb +84 -29
  26. data/lib/chef/encrypted_attribute/remote_nodes.rb +62 -11
  27. data/lib/chef/encrypted_attribute/remote_users.rb +58 -19
  28. data/lib/chef/encrypted_attribute/search_helper.rb +214 -74
  29. data/lib/chef/encrypted_attribute/version.rb +3 -1
  30. data/lib/chef/encrypted_attributes.rb +20 -0
  31. data/lib/chef/knife/core/config.rb +4 -1
  32. data/lib/chef/knife/core/encrypted_attribute_base.rb +179 -0
  33. data/lib/chef/knife/core/encrypted_attribute_depends.rb +43 -0
  34. data/lib/chef/knife/core/encrypted_attribute_editor_options.rb +125 -61
  35. data/lib/chef/knife/encrypted_attribute_create.rb +51 -31
  36. data/lib/chef/knife/encrypted_attribute_delete.rb +32 -40
  37. data/lib/chef/knife/encrypted_attribute_edit.rb +51 -32
  38. data/lib/chef/knife/encrypted_attribute_show.rb +30 -55
  39. data/lib/chef/knife/encrypted_attribute_update.rb +43 -28
  40. data/spec/benchmark_helper.rb +2 -1
  41. data/spec/integration_helper.rb +1 -0
  42. data/spec/spec_helper.rb +21 -7
  43. metadata +75 -36
  44. metadata.gz.sig +1 -1
  45. data/API.md +0 -174
  46. 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
- # This class is oriented to be easily integrable with
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
- VERSION_PREFIX = "#{self.name}::Version"
54
+ # Chef object type value.
55
+ CHEF_TYPE_VALUE = 'encrypted_attribute'.freeze
34
56
 
35
- def initialize(enc_hs=nil)
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.kind_of?(Hash)
71
+ update_from!(enc_hs) if enc_hs.is_a?(Hash)
40
72
  end
41
73
 
42
- %w{encrypt decrypt can_be_decrypted_by? needs_update?}.each do |meth|
74
+ %w(encrypt decrypt can_be_decrypted_by? needs_update?).each do |meth|
43
75
  define_method(meth) do
44
- raise NotImplementedError, "#{self.class.to_s}##{__method__} method not implemented."
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.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
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("#{self.name}.exists? is deprecated in favor of #{self.name}.exist?.")
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
- # Serialize this object as a Hash
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 for JSON
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
- # Update the EncryptedMash from Hash
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
- raise UnacceptableEncryptedAttributeFormat, 'Trying to construct invalid encrypted attribute. Maybe it is not encrypted?'
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
- # Create an EncryptedMash::Version from JSON Hash
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
- raise UnsupportedEncryptedAttributeFormat, "Unknown chef-encrypted-attribute class \"#{enc_hs[JSON_CLASS]}\""
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
- protected
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.kind_of?(String)
99
- raise UnacceptableEncryptedAttributeFormat, "Bad chef-encrypted-attribute class name \"#{class_name.inspect}\""
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
- if RUBY_VERSION < '1.9'
103
- class_name.split('::').inject(Kernel) { |scope, const| scope.const_get(const) }
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.kind_of?(String)
206
+ version = version.to_s unless version.is_a?(String)
115
207
  if version.empty?
116
- raise UnacceptableEncryptedAttributeFormat, "Bad chef-encrypted-attribute version \"#{version.inspect}\""
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
- raise UnsupportedEncryptedAttributeFormat, "This version of chef-encrypted-attribute does not support encrypted attribute item format version: \"#{version}\""
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 'yajl'
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'] = rsa_encrypt_multi_key(value_json, public_keys)
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
- parse_public_keys(keys).reduce(true) do |r, k|
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
- not can_be_decrypted_by?(keys) && self['encrypted_data'].keys.count == keys.count
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
- has_key?('encrypted_data') and self['encrypted_data'].kind_of?(Hash)
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.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}"
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
- raise InvalidPublicKey, 'Invalid public key provided.'
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? and key.private?
78
- raise InvalidPrivateKey, 'The provided key for decryption is invalid, a valid public and private key is required.'
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
- 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.'
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 = [ keys ].flatten
88
- keys.map do |k|
89
- parse_public_key(k)
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 Array or a Hash
95
- Yajl::Encoder.encode(o)
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
- Yajl::Parser.parse(o.to_s)
100
- rescue Yajl::ParseError => e
101
- raise DecryptionFailure, "#{e.class.name}: #{e.to_s}"
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
- # Heavily based on @sl4m code: https://gist.github.com/sl4m/1470360
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
- if RUBY_VERSION < '1.9.3'
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.to_s}"
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.to_s}"
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.has_key?(node_key(key.public_key))
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