0c3abb9d68d07c8b9c301dd5c44c910e5c46ef99
[packages/precise/mcollective.git] / lib / mcollective / ssl.rb
1 require 'openssl'
2 require 'base64'
3 require 'digest/sha1'
4
5 module MCollective
6   # A class that assists in encrypting and decrypting data using a
7   # combination of RSA and AES
8   #
9   # Data will be AES encrypted for speed, the Key used in # the AES
10   # stage will be encrypted using RSA
11   #
12   #   ssl = SSL.new(public_key, private_key, passphrase)
13   #
14   #   data = File.read("largefile.dat")
15   #
16   #   crypted_data = ssl.encrypt_with_private(data)
17   #
18   #   pp crypted_data
19   #
20   # This will result in a hash of data like:
21   #
22   #   crypted = {:key  => "crd4NHvG....=",
23   #              :data => "XWXlqN+i...=="}
24   #
25   # The key and data will all be base 64 encoded already by default
26   # you can pass a 2nd parameter as false to encrypt_with_private and
27   # counterparts that will prevent the base 64 encoding
28   #
29   # You can pass the data hash into ssl.decrypt_with_public which
30   # should return your original data
31   #
32   # There are matching methods for using a public key to encrypt
33   # data to be decrypted using a private key
34   class SSL
35     attr_reader :public_key_file, :private_key_file, :ssl_cipher
36
37     def initialize(pubkey=nil, privkey=nil, passphrase=nil, cipher=nil)
38       @public_key_file = pubkey
39       @private_key_file = privkey
40
41       @public_key  = read_key(:public, pubkey)
42       @private_key = read_key(:private, privkey, passphrase)
43
44       @ssl_cipher = "aes-256-cbc"
45       @ssl_cipher = Config.instance.ssl_cipher if Config.instance.ssl_cipher
46       @ssl_cipher = cipher if cipher
47
48       raise "The supplied cipher '#{@ssl_cipher}' is not supported" unless OpenSSL::Cipher.ciphers.include?(@ssl_cipher)
49     end
50
51     # Encrypts supplied data using AES and then encrypts using RSA
52     # the key and IV
53     #
54     # Return a hash with everything optionally base 64 encoded
55     def encrypt_with_public(plain_text, base64=true)
56       crypted = aes_encrypt(plain_text)
57
58       if base64
59         key = base64_encode(rsa_encrypt_with_public(crypted[:key]))
60         data = base64_encode(crypted[:data])
61       else
62         key = rsa_encrypt_with_public(crypted[:key])
63         data = crypted[:data]
64       end
65
66       {:key => key, :data => data}
67     end
68
69     # Encrypts supplied data using AES and then encrypts using RSA
70     # the key and IV
71     #
72     # Return a hash with everything optionally base 64 encoded
73     def encrypt_with_private(plain_text, base64=true)
74       crypted = aes_encrypt(plain_text)
75
76       if base64
77         key = base64_encode(rsa_encrypt_with_private(crypted[:key]))
78         data = base64_encode(crypted[:data])
79       else
80         key = rsa_encrypt_with_private(crypted[:key])
81         data = crypted[:data]
82       end
83
84       {:key => key, :data => data}
85     end
86
87     # Decrypts data, expects a hash as create with crypt_with_public
88     def decrypt_with_private(crypted, base64=true)
89       raise "Crypted data should include a key" unless crypted.include?(:key)
90       raise "Crypted data should include data" unless crypted.include?(:data)
91
92       if base64
93         key = rsa_decrypt_with_private(base64_decode(crypted[:key]))
94         aes_decrypt(key, base64_decode(crypted[:data]))
95       else
96         key = rsa_decrypt_with_private(crypted[:key])
97         aes_decrypt(key, crypted[:data])
98       end
99     end
100
101     # Decrypts data, expects a hash as create with crypt_with_private
102     def decrypt_with_public(crypted, base64=true)
103       raise "Crypted data should include a key" unless crypted.include?(:key)
104       raise "Crypted data should include data" unless crypted.include?(:data)
105
106       if base64
107         key = rsa_decrypt_with_public(base64_decode(crypted[:key]))
108         aes_decrypt(key, base64_decode(crypted[:data]))
109       else
110         key = rsa_decrypt_with_public(crypted[:key])
111         aes_decrypt(key, crypted[:data])
112       end
113     end
114
115     # Use the public key to RSA encrypt data
116     def rsa_encrypt_with_public(plain_string)
117       raise "No public key set" unless @public_key
118
119       @public_key.public_encrypt(plain_string)
120     end
121
122     # Use the private key to RSA decrypt data
123     def rsa_decrypt_with_private(crypt_string)
124       raise "No private key set" unless @private_key
125
126       @private_key.private_decrypt(crypt_string)
127     end
128
129     # Use the private key to RSA encrypt data
130     def rsa_encrypt_with_private(plain_string)
131       raise "No private key set" unless @private_key
132
133       @private_key.private_encrypt(plain_string)
134     end
135
136     # Use the public key to RSA decrypt data
137     def rsa_decrypt_with_public(crypt_string)
138       raise "No public key set" unless @public_key
139
140       @public_key.public_decrypt(crypt_string)
141     end
142
143     # encrypts a string, returns a hash of key, iv and data
144     def aes_encrypt(plain_string)
145       cipher = OpenSSL::Cipher::Cipher.new(ssl_cipher)
146       cipher.encrypt
147
148       key = cipher.random_key
149
150       cipher.key = key
151       cipher.pkcs5_keyivgen(key)
152       encrypted_data = cipher.update(plain_string) + cipher.final
153
154       {:key => key, :data => encrypted_data}
155     end
156
157     # decrypts a string given key, iv and data
158     def aes_decrypt(key, crypt_string)
159       cipher = OpenSSL::Cipher::Cipher.new(ssl_cipher)
160
161       cipher.decrypt
162       cipher.key = key
163       cipher.pkcs5_keyivgen(key)
164       decrypted_data = cipher.update(crypt_string) + cipher.final
165     end
166
167     # Signs a string using the private key
168     def sign(string, base64=false)
169       sig = @private_key.sign(OpenSSL::Digest::SHA1.new, string)
170
171       base64 ? base64_encode(sig) : sig
172     end
173
174     # Using the public key verifies that a string was signed using the private key
175     def verify_signature(signature, string, base64=false)
176       signature = base64_decode(signature) if base64
177
178       @public_key.verify(OpenSSL::Digest::SHA1.new, signature, string)
179     end
180
181     # base 64 encode a string
182     def base64_encode(string)
183       SSL.base64_encode(string)
184     end
185
186     def self.base64_encode(string)
187       Base64.encode64(string)
188     end
189
190     # base 64 decode a string
191     def base64_decode(string)
192       SSL.base64_decode(string)
193     end
194
195     def self.base64_decode(string)
196       Base64.decode64(string)
197     end
198
199     def md5(string)
200       SSL.md5(string)
201     end
202
203     def self.md5(string)
204       Digest::MD5.hexdigest(string)
205     end
206
207     # Creates a RFC 4122 version 5 UUID. If string is supplied it will produce repeatable
208     # UUIDs for that string else a random 128bit string will be used from OpenSSL::BN
209     #
210     # Code used with permission from:
211     #    https://github.com/kwilczynski/puppet-functions/blob/master/lib/puppet/parser/functions/uuid.rb
212     #
213     def self.uuid(string=nil)
214       string ||= OpenSSL::Random.random_bytes(16).unpack('H*').shift
215
216       uuid_name_space_dns = "\x6b\xa7\xb8\x10\x9d\xad\x11\xd1\x80\xb4\x00\xc0\x4f\xd4\x30\xc8"
217
218       sha1 = Digest::SHA1.new
219       sha1.update(uuid_name_space_dns)
220       sha1.update(string)
221
222       # first 16 bytes..
223       bytes = sha1.digest[0, 16].bytes.to_a
224
225       # version 5 adjustments
226       bytes[6] &= 0x0f
227       bytes[6] |= 0x50
228
229       # variant is DCE 1.1
230       bytes[8] &= 0x3f
231       bytes[8] |= 0x80
232
233       bytes = [4, 2, 2, 2, 6].collect do |i|
234         bytes.slice!(0, i).pack('C*').unpack('H*')
235       end
236
237       bytes.join('-')
238     end
239
240     # Reads either a :public or :private key from disk, uses an
241     # optional passphrase to read the private key
242     def read_key(type, key=nil, passphrase=nil)
243       return key if key.nil?
244
245       raise "Could not find key #{key}" unless File.exist?(key)
246       raise "#{type} key file '#{key}' is empty" if File.zero?(key)
247
248       if type == :public
249         begin
250           key = OpenSSL::PKey::RSA.new(File.read(key))
251         rescue OpenSSL::PKey::RSAError
252           key = OpenSSL::X509::Certificate.new(File.read(key)).public_key
253         end
254
255         # Ruby < 1.9.3 had a bug where it does not correctly clear the
256         # queue of errors while reading a key.  It tries various ways
257         # to read the key and each failing attempt pushes an error onto
258         # the queue.  With pubkeys only the 3rd attempt pass leaving 2
259         # stale errors on the error queue.
260         #
261         # In 1.9.3 they fixed this by simply discarding the errors after
262         # every attempt.  So we simulate this fix here for older rubies
263         # as without it we get SSL_read errors from the Stomp+TLS sessions
264         #
265         # We do this only on 1.8 relying on 1.9.3 to do the right thing
266         # and we do not support 1.9 less than 1.9.3
267         #
268         # See  http://bugs.ruby-lang.org/issues/4550
269         OpenSSL.errors if Util.ruby_version =~ /^1.8/
270
271         return key
272       elsif type == :private
273         return OpenSSL::PKey::RSA.new(File.read(key), passphrase)
274       else
275         raise "Can only load :public or :private keys"
276       end
277     end
278
279   end
280 end