Class: MCollective::SSL

Inherits:
Object
  • Object
show all
Defined in:
lib/mcollective/ssl.rb

Overview

A class that assists in encrypting and decrypting data using a combination of RSA and AES

Data will be AES encrypted for speed, the Key used in # the AES stage will be encrypted using RSA

ssl = SSL.new(public_key, private_key, passphrase)

data = File.read("largefile.dat")

crypted_data = ssl.encrypt_with_private(data)

pp crypted_data

This will result in a hash of data like:

crypted = {:key  => "crd4NHvG....=",
           :data => "XWXlqN+i...=="}

The key and data will all be base 64 encoded already by default you can pass a 2nd parameter as false to encrypt_with_private and counterparts that will prevent the base 64 encoding

You can pass the data hash into ssl.decrypt_with_public which should return your original data

There are matching methods for using a public key to encrypt data to be decrypted using a private key

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pubkey = nil, privkey = nil, passphrase = nil, cipher = nil) ⇒ SSL

Returns a new instance of SSL.



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/mcollective/ssl.rb', line 37

def initialize(pubkey=nil, privkey=nil, passphrase=nil, cipher=nil)
  @public_key_file = pubkey
  @private_key_file = privkey

  @public_key  = read_key(:public, pubkey)
  @private_key = read_key(:private, privkey, passphrase)

  @ssl_cipher = "aes-256-cbc"
  @ssl_cipher = Config.instance.ssl_cipher if Config.instance.ssl_cipher
  @ssl_cipher = cipher if cipher

  raise "The supplied cipher '#{@ssl_cipher}' is not supported" unless OpenSSL::Cipher.ciphers.include?(@ssl_cipher)
end

Instance Attribute Details

#private_key_fileObject (readonly)

Returns the value of attribute private_key_file.



35
36
37
# File 'lib/mcollective/ssl.rb', line 35

def private_key_file
  @private_key_file
end

#public_key_fileObject (readonly)

Returns the value of attribute public_key_file.



35
36
37
# File 'lib/mcollective/ssl.rb', line 35

def public_key_file
  @public_key_file
end

#ssl_cipherObject (readonly)

Returns the value of attribute ssl_cipher.



35
36
37
# File 'lib/mcollective/ssl.rb', line 35

def ssl_cipher
  @ssl_cipher
end

Class Method Details

.base64_decode(string) ⇒ Object



195
196
197
198
199
200
201
202
# File 'lib/mcollective/ssl.rb', line 195

def self.base64_decode(string)
  # The Base 64 character set is A-Z a-z 0-9 + / =
  # Also allow for whitespace, but raise if we get anything else
  if string !~ /^[A-Za-z0-9+\/=\s]+$/
    raise ArgumentError, 'invalid base64'
  end
  Base64.decode64(string)
end

.base64_encode(string) ⇒ Object



186
187
188
# File 'lib/mcollective/ssl.rb', line 186

def self.base64_encode(string)
  Base64.encode64(string)
end

.md5(string) ⇒ Object



208
209
210
# File 'lib/mcollective/ssl.rb', line 208

def self.md5(string)
  Digest::MD5.hexdigest(string)
end

.uuid(string = nil) ⇒ Object

Creates a RFC 4122 version 5 UUID. If string is supplied it will produce repeatable UUIDs for that string else a random 128bit string will be used from OpenSSL::BN

Code used with permission from:

https://github.com/kwilczynski/puppet-functions/blob/master/lib/puppet/parser/functions/uuid.rb


218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/mcollective/ssl.rb', line 218

def self.uuid(string=nil)
  string ||= OpenSSL::Random.random_bytes(16).unpack('H*').shift

  uuid_name_space_dns = [0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8].map {|b| b.chr}.join

  sha1 = Digest::SHA1.new
  sha1.update(uuid_name_space_dns)
  sha1.update(string)

  # first 16 bytes..
  bytes = sha1.digest[0, 16].bytes.to_a

  # version 5 adjustments
  bytes[6] &= 0x0f
  bytes[6] |= 0x50

  # variant is DCE 1.1
  bytes[8] &= 0x3f
  bytes[8] |= 0x80

  bytes = [4, 2, 2, 2, 6].collect do |i|
    bytes.slice!(0, i).pack('C*').unpack('H*')
  end

  bytes.join('-')
end

Instance Method Details

#aes_decrypt(key, crypt_string) ⇒ Object

decrypts a string given key, iv and data



158
159
160
161
162
163
164
165
# File 'lib/mcollective/ssl.rb', line 158

def aes_decrypt(key, crypt_string)
  cipher = OpenSSL::Cipher.new(ssl_cipher)

  cipher.decrypt
  cipher.key = key
  cipher.pkcs5_keyivgen(key)
  decrypted_data = cipher.update(crypt_string) + cipher.final
end

#aes_encrypt(plain_string) ⇒ Object

encrypts a string, returns a hash of key, iv and data



144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/mcollective/ssl.rb', line 144

def aes_encrypt(plain_string)
  cipher = OpenSSL::Cipher.new(ssl_cipher)
  cipher.encrypt

  key = cipher.random_key

  cipher.key = key
  cipher.pkcs5_keyivgen(key)
  encrypted_data = cipher.update(plain_string) + cipher.final

  {:key => key, :data => encrypted_data}
end

#base64_decode(string) ⇒ Object

base 64 decode a string



191
192
193
# File 'lib/mcollective/ssl.rb', line 191

def base64_decode(string)
  SSL.base64_decode(string)
end

#base64_encode(string) ⇒ Object

base 64 encode a string



182
183
184
# File 'lib/mcollective/ssl.rb', line 182

def base64_encode(string)
  SSL.base64_encode(string)
end

#decrypt_with_private(crypted, base64 = true) ⇒ Object

Decrypts data, expects a hash as create with crypt_with_public



88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/mcollective/ssl.rb', line 88

def decrypt_with_private(crypted, base64=true)
  raise "Crypted data should include a key" unless crypted.include?(:key)
  raise "Crypted data should include data" unless crypted.include?(:data)

  if base64
    key = rsa_decrypt_with_private(base64_decode(crypted[:key]))
    aes_decrypt(key, base64_decode(crypted[:data]))
  else
    key = rsa_decrypt_with_private(crypted[:key])
    aes_decrypt(key, crypted[:data])
  end
end

#decrypt_with_public(crypted, base64 = true) ⇒ Object

Decrypts data, expects a hash as create with crypt_with_private



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/mcollective/ssl.rb', line 102

def decrypt_with_public(crypted, base64=true)
  raise "Crypted data should include a key" unless crypted.include?(:key)
  raise "Crypted data should include data" unless crypted.include?(:data)

  if base64
    key = rsa_decrypt_with_public(base64_decode(crypted[:key]))
    aes_decrypt(key, base64_decode(crypted[:data]))
  else
    key = rsa_decrypt_with_public(crypted[:key])
    aes_decrypt(key, crypted[:data])
  end
end

#encrypt_with_private(plain_text, base64 = true) ⇒ Object

Encrypts supplied data using AES and then encrypts using RSA the key and IV

Return a hash with everything optionally base 64 encoded



73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/mcollective/ssl.rb', line 73

def encrypt_with_private(plain_text, base64=true)
  crypted = aes_encrypt(plain_text)

  if base64
    key = base64_encode(rsa_encrypt_with_private(crypted[:key]))
    data = base64_encode(crypted[:data])
  else
    key = rsa_encrypt_with_private(crypted[:key])
    data = crypted[:data]
  end

  {:key => key, :data => data}
end

#encrypt_with_public(plain_text, base64 = true) ⇒ Object

Encrypts supplied data using AES and then encrypts using RSA the key and IV

Return a hash with everything optionally base 64 encoded



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/mcollective/ssl.rb', line 55

def encrypt_with_public(plain_text, base64=true)
  crypted = aes_encrypt(plain_text)

  if base64
    key = base64_encode(rsa_encrypt_with_public(crypted[:key]))
    data = base64_encode(crypted[:data])
  else
    key = rsa_encrypt_with_public(crypted[:key])
    data = crypted[:data]
  end

  {:key => key, :data => data}
end

#md5(string) ⇒ Object



204
205
206
# File 'lib/mcollective/ssl.rb', line 204

def md5(string)
  SSL.md5(string)
end

#read_key(type, key = nil, passphrase = nil) ⇒ Object

Reads either a :public or :private key from disk, uses an optional passphrase to read the private key



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/mcollective/ssl.rb', line 247

def read_key(type, key=nil, passphrase=nil)
  return key if key.nil?

  raise "Could not find key #{key}" unless File.exist?(key)
  raise "#{type} key file '#{key}' is empty" if File.zero?(key)

  if type == :public
    begin
      key = OpenSSL::PKey::RSA.new(File.read(key))
    rescue OpenSSL::PKey::RSAError
      key = OpenSSL::X509::Certificate.new(File.read(key)).public_key
    end

    # Ruby < 1.9.3 had a bug where it does not correctly clear the
    # queue of errors while reading a key.  It tries various ways
    # to read the key and each failing attempt pushes an error onto
    # the queue.  With pubkeys only the 3rd attempt pass leaving 2
    # stale errors on the error queue.
    #
    # In 1.9.3 they fixed this by simply discarding the errors after
    # every attempt.  So we simulate this fix here for older rubies
    # as without it we get SSL_read errors from the Stomp+TLS sessions
    #
    # We do this only on 1.8 relying on 1.9.3 to do the right thing
    # and we do not support 1.9 less than 1.9.3
    #
    # See  http://bugs.ruby-lang.org/issues/4550
    OpenSSL.errors if Util.ruby_version =~ /^1.8/

    return key
  elsif type == :private
    return OpenSSL::PKey::RSA.new(File.read(key), passphrase)
  else
    raise "Can only load :public or :private keys"
  end
end

#rsa_decrypt_with_private(crypt_string) ⇒ Object

Use the private key to RSA decrypt data



123
124
125
126
127
# File 'lib/mcollective/ssl.rb', line 123

def rsa_decrypt_with_private(crypt_string)
  raise "No private key set" unless @private_key

  @private_key.private_decrypt(crypt_string)
end

#rsa_decrypt_with_public(crypt_string) ⇒ Object

Use the public key to RSA decrypt data



137
138
139
140
141
# File 'lib/mcollective/ssl.rb', line 137

def rsa_decrypt_with_public(crypt_string)
  raise "No public key set" unless @public_key

  @public_key.public_decrypt(crypt_string)
end

#rsa_encrypt_with_private(plain_string) ⇒ Object

Use the private key to RSA encrypt data



130
131
132
133
134
# File 'lib/mcollective/ssl.rb', line 130

def rsa_encrypt_with_private(plain_string)
  raise "No private key set" unless @private_key

  @private_key.private_encrypt(plain_string)
end

#rsa_encrypt_with_public(plain_string) ⇒ Object

Use the public key to RSA encrypt data



116
117
118
119
120
# File 'lib/mcollective/ssl.rb', line 116

def rsa_encrypt_with_public(plain_string)
  raise "No public key set" unless @public_key

  @public_key.public_encrypt(plain_string)
end

#sign(string, base64 = false) ⇒ Object

Signs a string using the private key



168
169
170
171
172
# File 'lib/mcollective/ssl.rb', line 168

def sign(string, base64=false)
  sig = @private_key.sign(OpenSSL::Digest::SHA1.new, string)

  base64 ? base64_encode(sig) : sig
end

#verify_signature(signature, string, base64 = false) ⇒ Object

Using the public key verifies that a string was signed using the private key



175
176
177
178
179
# File 'lib/mcollective/ssl.rb', line 175

def verify_signature(signature, string, base64=false)
  signature = base64_decode(signature) if base64

  @public_key.verify(OpenSSL::Digest::SHA1.new, signature, string)
end