Module: Crypto

Defined in:
lib/ec2/amitools/crypto.rb

Overview

Cryptographic utilities module.

Constant Summary collapse

BUFFER_SIZE =
1024 * 1024
ASYM_ALG =
'RSA'
SYM_ALG =
'AES-128-CBC'
DIGEST_ALG =
'SHA1'
PADDING =
OpenSSL::PKey::RSA::PKCS1_PADDING
VERSION1 =
1
VERSION2 =
2
SHA1_FINGERPRINT_REGEX =
/([a-f0-9]{2}(:[a-f0-9]{2}){15})/i

Class Method Summary collapse

Class Method Details

.authenticate(data, sig, pubkey) ⇒ Object

Verify the authenticity of the data from the IO stream or string ((|data|)) using the signature ((|sig|)) and the public key ((|pubkey|)).

Return true iff the signature is valid.

Raises:

  • (ArgumentError)


138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/ec2/amitools/crypto.rb', line 138

def Crypto.authenticate(data, sig, pubkey)
  raise ArgumentError.new("Invalid parameter data") if data.nil?
  raise ArgumentError.new("Invalid parameter sig") if sig.nil? or sig.length==0
  raise ArgumentError.new("Invalid parameter pubkey") if pubkey.nil?

  # Create IO stream if necessary.
  io = (data.instance_of?(StringIO) ? data : StringIO.new(data))

  sha = OpenSSL::Digest::SHA1.new
  res = false
  while not (io.eof?)
    res = pubkey.verify(sha, sig, io.read(BUFFER_SIZE))
  end
  res
end

.cert2pubkey(data) ⇒ Object

—————————————————————————-#



301
302
303
304
305
306
307
# File 'lib/ec2/amitools/crypto.rb', line 301

def Crypto.cert2pubkey(data)
  begin
    return OpenSSL::X509::Certificate.new(data).public_key
  rescue Exception => e
    raise "error reading certificate: #{e.message}"
  end
end

.cert_sha1_fingerprint(cert_filename) ⇒ Object

Generate the SHA1 fingerprint for a PEM-encoded certificate (NOT private key) Returns the fingerprint in aa:bb:… form Raises ArgumentError if the fingerprint cannot be obtained

Raises:

  • (ArgumentError)


336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/ec2/amitools/crypto.rb', line 336

def Crypto.cert_sha1_fingerprint(cert_filename)
  raise ArgumentError.new('cert_filename is nil')  if cert_filename.nil?
  raise ArgumentError.new("invalid cert file name: #{cert_filename}") unless FileTest.exists?(cert_filename)
  fingerprint = nil

  IO.popen("openssl x509 -in #{cert_filename} -noout -sha1 -fingerprint") do |io|
    out = io.read
    md = SHA1_FINGERPRINT_REGEX.match(out)
    if md
      fingerprint = md[1]
    end
  end

  raise ArgumentError.new("could not generate fingerprint for #{cert_filename}")  if fingerprint.nil?

  return fingerprint
end

.certfile2pubkey(filename) ⇒ Object

Return the public key from the X509 certificate file ((|filename|)).



289
290
291
292
293
294
295
296
297
# File 'lib/ec2/amitools/crypto.rb', line 289

def Crypto.certfile2pubkey(filename)
  begin
    File.open(filename) do |f|
      return cert2pubkey(f)
    end
  rescue Exception => e
    raise "error reading certificate file #{filename}: #{e.message}"
  end
end

.decryptasym(cipher_text, keyfilename) ⇒ Object

Decrypt the specified cipher text according to the AMI Manifest Encryption Scheme Version 1 or 2.

((|cipher_text|)) The cipher text to decrypt. ((|keyio_or_keyfilename|)) The key data IO stream or the name of the private key file.

Raises:

  • (ArgumentError)


39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/ec2/amitools/crypto.rb', line 39

def Crypto.decryptasym(cipher_text, keyfilename)
  raise ArgumentError.new('cipher_text') unless cipher_text
  raise ArgumentError.new('keyfilename') unless keyfilename and FileTest.exists? keyfilename

  # Load key.
  privkey = File.open(keyfilename, 'r') { |f| OpenSSL::PKey::RSA.new(f) }

  # Get version.
  version = cipher_text[0]
  if version == VERSION2
    return Crypto.decryptasym_v2( cipher_text, keyfilename )
  end
  raise ArgumentError.new("invalid encryption scheme versionb: #{version}") unless version == 1

  # Decrypt and extract encrypted symmetric key and initialization vector.
  symkey_cryptogram_len = cipher_text.slice(1, 2).unpack('C')[0]
  symkey_cryptogram = privkey.private_decrypt(
    cipher_text.slice(2, symkey_cryptogram_len),
    PADDING)
  symkey = symkey_cryptogram.slice(0, 16)
  iv = symkey_cryptogram.slice(16, 16)

  # Decrypt data with the symmetric key.
  cryptogram = cipher_text.slice(2 + symkey_cryptogram_len..cipher_text.size)
  decryptsym(cryptogram, symkey, iv)
end

.decryptasym_v2(cipher_text, keyfilename) ⇒ Object

Decrypt the specified cipher text according to the AMI Manifest Encryption Scheme Version 2.

((|cipher_text|)) The cipher text to decrypt. ((|keyio_or_keyfilename|)) The key data IO stream or the name of the private key file.

Raises:

  • (ArgumentError)


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/ec2/amitools/crypto.rb', line 76

def Crypto.decryptasym_v2(cipher_text, keyfilename)
  raise ArgumentError.new('cipher_text') unless cipher_text
  raise ArgumentError.new('keyfilename') unless keyfilename and FileTest.exists? keyfilename

  # Load key.
  privkey = File.open(keyfilename, 'r') { |f| OpenSSL::PKey::RSA.new(f) }

  # Get version.
  version = cipher_text[0]
  raise ArgumentError.new("invalid encryption scheme versionb: #{version}") unless version == VERSION2

  # Decrypt and extract encrypted symmetric key and initialization vector.
  hi_byte, lo_byte = cipher_text.slice(1, 3).unpack('CC')
  symkey_cryptogram_len = ( hi_byte << 8 ) | lo_byte
  symkey_cryptogram = privkey.private_decrypt(
    cipher_text.slice(3, symkey_cryptogram_len),
    PADDING)
  
  symkey = symkey_cryptogram.slice(0, 16)
  iv = symkey_cryptogram.slice(16, 16)

  # Decrypt data with the symmetric key.
  cryptogram = cipher_text.slice( ( 3 + symkey_cryptogram_len )..cipher_text.size)
  decryptsym(cryptogram, symkey, iv)
end

.decryptfile(src, dst, key, iv) ⇒ Object

Decrypt the specified cipher text file to create the specified plain text file.

The symmetric cipher is AES in CBC mode. 128 bit keys are used. If the plain text file already exists it will be overwritten.

((|src|)) The name of the cipher text file to decrypt. ((|dst|)) The name of the plain text file to create. ((|key|)) The 128 bit (16 byte) symmetric key. ((|iv|)) The 128 bit (16 byte) initialization vector.

Raises:

  • (ArgumentError)


168
169
170
171
172
173
174
175
176
# File 'lib/ec2/amitools/crypto.rb', line 168

def Crypto.decryptfile(src, dst, key, iv)
  raise ArgumentError.new("invalid file name: #{src}") unless FileTest.exists?(src)
  raise ArgumentError.new("invalid key") unless key and key.size == 16
  raise ArgumentError.new("invalid iv") unless iv and iv.size == 16
  pio = IO.popen( "openssl enc -d -aes-128-cbc -in #{src} -out #{dst} -K #{Format::bin2hex(key)} -iv #{Format::bin2hex(iv)} 2>&1" )
  result = pio.read
  pio.close
  raise "error decrypting file #{src}: #{result}" if result.strip != ''
end

.decryptsym(plaintext, key, iv) ⇒ Object

Decrypt ciphertext using key and iv using AES-128-CBC.

Raises:

  • (ArgumentError)


183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/ec2/amitools/crypto.rb', line 183

def Crypto.decryptsym(plaintext, key, iv)
  raise ArgumentError.new("plaintext must be a String") unless plaintext.is_a? String
  raise ArgumentError.new("invalid key") unless key.is_a? String and key.size == 16
  raise ArgumentError.new("invalid iv") unless iv.is_a? String and iv.size == 16

  cipher = OpenSSL::Cipher::Cipher.new( 'AES-128-CBC' )
  cipher.decrypt( key, iv )
  # NOTE: If the key and iv aren't set this doesn't work correctly.
  cipher.key = key
  cipher.iv = iv
  plaintext = cipher.update( plaintext )
  plaintext + cipher.final
end

.digest(io, alg = OpenSSL::Digest::SHA1.new) ⇒ Object

Generate and return a message digest for the data from the IO stream ((|io|)), using the algorithm alg

Raises:

  • (ArgumentError)


203
204
205
206
207
208
209
# File 'lib/ec2/amitools/crypto.rb', line 203

def Crypto.digest(io, alg = OpenSSL::Digest::SHA1.new)
  raise ArgumentError.new('io') unless io.kind_of?(IO) or io.kind_of?(StringIO)
  while not io.eof?
    alg.update(io.read(BUFFER_SIZE))
  end
  alg.digest
end

.encryptasym(data, pubkey) ⇒ Object

Asymmetrically encrypt the specified data using the AMI Manifest Encryption Scheme Version 2.

The data is encrypted with an ephemeral symmetric key and initialization vector. The symmetric key and initialization vector are encrypted with the specified public key and preprended to the data.

((|data|)) The data to encrypt. ((|pubkey|)) The public key.

Raises:

  • (ArgumentError)


115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/ec2/amitools/crypto.rb', line 115

def Crypto.encryptasym(data, pubkey)
  raise ArgumentError.new('data') unless data
  raise ArgumentError.new('pubkey') unless pubkey

  symkey = gensymkey
  iv = geniv
  symkey_cryptogram = pubkey.public_encrypt( symkey + iv, PADDING )

  data_cryptogram = encryptsym(data, symkey, iv)

  hi_byte, lo_byte = Format.int2int16(symkey_cryptogram.size)

  Format::int2byte(VERSION2) + hi_byte + lo_byte + symkey_cryptogram + data_cryptogram
end

.encryptfile(src, dst, key, iv) ⇒ Object

Decrypt the specified cipher text file to create the specified plain text file.

The symmetric cipher is AES in CBC mode. 128 bit keys are used. If the plain text file already exists it will be overwritten.

((|key|)) The 128 bit (16 byte) symmetric key. ((|src|)) The name of the cipher text file to encrypt. ((|dst|)) The name of the plain text file to create. ((|iv|)) The 128 bit (16 byte) initialization vector.

Raises:

  • (ArgumentError)


238
239
240
241
242
243
244
245
# File 'lib/ec2/amitools/crypto.rb', line 238

def Crypto.encryptfile(src, dst, key, iv)
  raise ArgumentError.new("invalid file name: #{src}") unless FileTest.exists?(src)
  raise ArgumentError.new("invalid key") unless key and key.size == 16
  raise ArgumentError.new("invalid iv") unless iv and iv.size == 16
  cmd = "openssl enc -e -aes-128-cbc -in #{src} -out #{dst} -K #{Format::bin2hex(key)} -iv #{Format::bin2hex(iv)}"
  result = Kernel::system(cmd)
  raise "error encrypting file #{src}" unless result
end

.encryptsym(plaintext, key, iv) ⇒ Object

Encrypt plaintext with key and iv using AES-128-CBC.

Raises:

  • (ArgumentError)


252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/ec2/amitools/crypto.rb', line 252

def Crypto.encryptsym(plaintext, key, iv)
  raise ArgumentError.new("plaintext must be a String") unless plaintext.kind_of? String
  raise ArgumentError.new("invalid key") unless ( key.is_a? String and key.size == 16 )
  raise ArgumentError.new("invalid iv") unless ( iv.is_a? String and iv.size == 16 )

  cipher = OpenSSL::Cipher::Cipher.new( 'AES-128-CBC' )
  cipher.encrypt( key, iv )
  # NOTE: If the key and iv aren't set this doesn't work correctly.
  cipher.key = key
  cipher.iv = iv
  ciphertext = cipher.update( plaintext )
  ciphertext + cipher.final
end

.genivObject

Generate an initialization vector suitable use with symmetric cipher.



271
272
273
# File 'lib/ec2/amitools/crypto.rb', line 271

def Crypto.geniv
  OpenSSL::Cipher::Cipher.new(SYM_ALG).random_iv
end

.gensymkeyObject

Generate a key suitable for use with a symmetric cipher.



280
281
282
# File 'lib/ec2/amitools/crypto.rb', line 280

def Crypto.gensymkey
  OpenSSL::Cipher::Cipher.new(SYM_ALG).random_key
end

.hmac_sha1(key, data) ⇒ Object

Return the HMAC SHA1 of data using key.

Raises:



214
215
216
217
218
219
220
221
222
# File 'lib/ec2/amitools/crypto.rb', line 214

def Crypto.hmac_sha1( key, data )
  raise ParameterError.new( "key must be a String" ) unless key.is_a? String
  raise ParameterError.new( "data must be a String" ) unless data.is_a? String

  md = OpenSSL::Digest::SHA1.new
  hmac = OpenSSL::HMAC.new( key, md)
  hmac.update( data )
  return hmac.digest
end

.loadprivkey(filename) ⇒ Object

——————————————————————————#



356
357
358
359
360
361
362
# File 'lib/ec2/amitools/crypto.rb', line 356

def Crypto.loadprivkey filename
  begin
    OpenSSL::PKey::RSA.new( File.open( filename,'r' ) )
  rescue Exception => e
    raise "error reading private key from file #{filename}: #{e.message}"
  end
end

.sign(data, keyfilename) ⇒ Object

Sign the data from IO stream or string ((|data|)) using the key in ((|keyfilename|)).

Return the signature.

Raises:

  • (ArgumentError)


317
318
319
320
321
322
323
324
325
326
327
# File 'lib/ec2/amitools/crypto.rb', line 317

def Crypto.sign(data, keyfilename)
  raise ArgumentError.new('data') unless data
  raise ArgumentError.new("invalid file name: #{keyfilename}") unless FileTest.exists?(keyfilename)

  # Create an IO stream from the data if necessary.
  io = (data.instance_of?(StringIO) ? data : StringIO.new(data))

  sha = OpenSSL::Digest::SHA1.new
  pk  = loadprivkey( keyfilename )
  return pk.sign(sha, io.read )
end

.xor(a, b) ⇒ Object

XOR the byte string ((|a|)) with the byte string ((|b|)). The operans must be of the same length.

Raises:

  • (ArgumentError)


370
371
372
373
374
375
376
377
378
# File 'lib/ec2/amitools/crypto.rb', line 370

def Crypto.xor(a, b)
  raise ArgumentError.new('data lengths differ') unless a.size == b.size
  xored = String.new
  a.size.times do |i|
    xored << (a[i] ^ b[i])
  end

  xored
end