1
# Copyright 2008 Amazon.com, Inc. or its affiliates. All Rights
2
# Reserved. Licensed under the Amazon Software License (the
3
# "License"). You may not use this file except in compliance with the
4
# License. A copy of the License is located at
5
# http://aws.amazon.com/asl or in the "license" file accompanying this
6
# file. This file is distributed on an "AS IS" BASIS, WITHOUT
7
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
8
# the License for the specific language governing permissions and
9
# limitations under the License.
11
require 'ec2/amitools/format'
17
# Cryptographic utilities module.
20
BUFFER_SIZE = 1024 * 1024
22
SYM_ALG = 'AES-128-CBC'
24
PADDING = OpenSSL::PKey::RSA::PKCS1_PADDING
27
SHA1_FINGERPRINT_REGEX = /([a-f0-9]{2}(:[a-f0-9]{2}){15})/i
29
#----------------------------------------------------------------------------#
32
# Decrypt the specified cipher text according to the AMI Manifest Encryption
33
# Scheme Version 1 or 2.
35
# ((|cipher_text|)) The cipher text to decrypt.
36
# ((|keyio_or_keyfilename|)) The key data IO stream or the name of the private
39
def Crypto.decryptasym(cipher_text, keyfilename)
40
raise ArgumentError.new('cipher_text') unless cipher_text
41
raise ArgumentError.new('keyfilename') unless keyfilename and FileTest.exists? keyfilename
44
privkey = File.open(keyfilename, 'r') { |f| OpenSSL::PKey::RSA.new(f) }
47
version = cipher_text[0]
48
if version == VERSION2
49
return Crypto.decryptasym_v2( cipher_text, keyfilename )
51
raise ArgumentError.new("invalid encryption scheme versionb: #{version}") unless version == 1
53
# Decrypt and extract encrypted symmetric key and initialization vector.
54
symkey_cryptogram_len = cipher_text.slice(1, 2).unpack('C')[0]
55
symkey_cryptogram = privkey.private_decrypt(
56
cipher_text.slice(2, symkey_cryptogram_len),
58
symkey = symkey_cryptogram.slice(0, 16)
59
iv = symkey_cryptogram.slice(16, 16)
61
# Decrypt data with the symmetric key.
62
cryptogram = cipher_text.slice(2 + symkey_cryptogram_len..cipher_text.size)
63
decryptsym(cryptogram, symkey, iv)
66
#----------------------------------------------------------------------------#
69
# Decrypt the specified cipher text according to the AMI Manifest Encryption
72
# ((|cipher_text|)) The cipher text to decrypt.
73
# ((|keyio_or_keyfilename|)) The key data IO stream or the name of the private
76
def Crypto.decryptasym_v2(cipher_text, keyfilename)
77
raise ArgumentError.new('cipher_text') unless cipher_text
78
raise ArgumentError.new('keyfilename') unless keyfilename and FileTest.exists? keyfilename
81
privkey = File.open(keyfilename, 'r') { |f| OpenSSL::PKey::RSA.new(f) }
84
version = cipher_text[0]
85
raise ArgumentError.new("invalid encryption scheme versionb: #{version}") unless version == VERSION2
87
# Decrypt and extract encrypted symmetric key and initialization vector.
88
hi_byte, lo_byte = cipher_text.slice(1, 3).unpack('CC')
89
symkey_cryptogram_len = ( hi_byte << 8 ) | lo_byte
90
symkey_cryptogram = privkey.private_decrypt(
91
cipher_text.slice(3, symkey_cryptogram_len),
94
symkey = symkey_cryptogram.slice(0, 16)
95
iv = symkey_cryptogram.slice(16, 16)
97
# Decrypt data with the symmetric key.
98
cryptogram = cipher_text.slice( ( 3 + symkey_cryptogram_len )..cipher_text.size)
99
decryptsym(cryptogram, symkey, iv)
102
#----------------------------------------------------------------------------#
105
# Asymmetrically encrypt the specified data using the AMI Manifest Encryption
108
# The data is encrypted with an ephemeral symmetric key and initialization
109
# vector. The symmetric key and initialization vector are encrypted with the
110
# specified public key and preprended to the data.
112
# ((|data|)) The data to encrypt.
113
# ((|pubkey|)) The public key.
115
def Crypto.encryptasym(data, pubkey)
116
raise ArgumentError.new('data') unless data
117
raise ArgumentError.new('pubkey') unless pubkey
121
symkey_cryptogram = pubkey.public_encrypt( symkey + iv, PADDING )
123
data_cryptogram = encryptsym(data, symkey, iv)
125
hi_byte, lo_byte = Format.int2int16(symkey_cryptogram.size)
127
Format::int2byte(VERSION2) + hi_byte + lo_byte + symkey_cryptogram + data_cryptogram
130
#----------------------------------------------------------------------------#
133
# Verify the authenticity of the data from the IO stream or string ((|data|))
134
# using the signature ((|sig|)) and the public key ((|pubkey|)).
136
# Return true iff the signature is valid.
138
def Crypto.authenticate(data, sig, pubkey)
139
raise ArgumentError.new("Invalid parameter data") if data.nil?
140
raise ArgumentError.new("Invalid parameter sig") if sig.nil? or sig.length==0
141
raise ArgumentError.new("Invalid parameter pubkey") if pubkey.nil?
143
# Create IO stream if necessary.
144
io = (data.instance_of?(StringIO) ? data : StringIO.new(data))
146
sha = OpenSSL::Digest::SHA1.new
149
res = pubkey.verify(sha, sig, io.read(BUFFER_SIZE))
154
#----------------------------------------------------------------------------#
157
# Decrypt the specified cipher text file to create the specified plain text
160
# The symmetric cipher is AES in CBC mode. 128 bit keys are used. If the plain
161
# text file already exists it will be overwritten.
163
# ((|src|)) The name of the cipher text file to decrypt.
164
# ((|dst|)) The name of the plain text file to create.
165
# ((|key|)) The 128 bit (16 byte) symmetric key.
166
# ((|iv|)) The 128 bit (16 byte) initialization vector.
168
def Crypto.decryptfile(src, dst, key, iv)
169
raise ArgumentError.new("invalid file name: #{src}") unless FileTest.exists?(src)
170
raise ArgumentError.new("invalid key") unless key and key.size == 16
171
raise ArgumentError.new("invalid iv") unless iv and iv.size == 16
172
pio = IO.popen( "openssl enc -d -aes-128-cbc -in #{src} -out #{dst} -K #{Format::bin2hex(key)} -iv #{Format::bin2hex(iv)} 2>&1" )
175
raise "error decrypting file #{src}: #{result}" if result.strip != ''
178
#----------------------------------------------------------------------------#
181
# Decrypt _ciphertext_ using _key_ and _iv_ using AES-128-CBC.
183
def Crypto.decryptsym(plaintext, key, iv)
184
raise ArgumentError.new("plaintext must be a String") unless plaintext.is_a? String
185
raise ArgumentError.new("invalid key") unless key.is_a? String and key.size == 16
186
raise ArgumentError.new("invalid iv") unless iv.is_a? String and iv.size == 16
188
cipher = OpenSSL::Cipher::Cipher.new( 'AES-128-CBC' )
189
cipher.decrypt( key, iv )
190
# NOTE: If the key and iv aren't set this doesn't work correctly.
193
plaintext = cipher.update( plaintext )
194
plaintext + cipher.final
197
#----------------------------------------------------------------------------#
200
# Generate and return a message digest for the data from the IO stream
201
# ((|io|)), using the algorithm alg
203
def Crypto.digest(io, alg = OpenSSL::Digest::SHA1.new)
204
raise ArgumentError.new('io') unless io.kind_of?(IO) or io.kind_of?(StringIO)
206
alg.update(io.read(BUFFER_SIZE))
211
#----------------------------------------------------------------------------#
213
# Return the HMAC SHA1 of _data_ using _key_.
214
def Crypto.hmac_sha1( key, data )
215
raise ParameterError.new( "key must be a String" ) unless key.is_a? String
216
raise ParameterError.new( "data must be a String" ) unless data.is_a? String
218
md = OpenSSL::Digest::SHA1.new
219
hmac = OpenSSL::HMAC.new( key, md)
224
#----------------------------------------------------------------------------#
227
# Decrypt the specified cipher text file to create the specified plain text
230
# The symmetric cipher is AES in CBC mode. 128 bit keys are used. If the plain
231
# text file already exists it will be overwritten.
233
# ((|key|)) The 128 bit (16 byte) symmetric key.
234
# ((|src|)) The name of the cipher text file to encrypt.
235
# ((|dst|)) The name of the plain text file to create.
236
# ((|iv|)) The 128 bit (16 byte) initialization vector.
238
def Crypto.encryptfile(src, dst, key, iv)
239
raise ArgumentError.new("invalid file name: #{src}") unless FileTest.exists?(src)
240
raise ArgumentError.new("invalid key") unless key and key.size == 16
241
raise ArgumentError.new("invalid iv") unless iv and iv.size == 16
242
cmd = "openssl enc -e -aes-128-cbc -in #{src} -out #{dst} -K #{Format::bin2hex(key)} -iv #{Format::bin2hex(iv)}"
243
result = Kernel::system(cmd)
244
raise "error encrypting file #{src}" unless result
247
#----------------------------------------------------------------------------#
250
# Encrypt _plaintext_ with _key_ and _iv_ using AES-128-CBC.
252
def Crypto.encryptsym(plaintext, key, iv)
253
raise ArgumentError.new("plaintext must be a String") unless plaintext.kind_of? String
254
raise ArgumentError.new("invalid key") unless ( key.is_a? String and key.size == 16 )
255
raise ArgumentError.new("invalid iv") unless ( iv.is_a? String and iv.size == 16 )
257
cipher = OpenSSL::Cipher::Cipher.new( 'AES-128-CBC' )
258
cipher.encrypt( key, iv )
259
# NOTE: If the key and iv aren't set this doesn't work correctly.
262
ciphertext = cipher.update( plaintext )
263
ciphertext + cipher.final
266
#----------------------------------------------------------------------------#
269
# Generate an initialization vector suitable use with symmetric cipher.
272
OpenSSL::Cipher::Cipher.new(SYM_ALG).random_iv
275
#----------------------------------------------------------------------------#
278
# Generate a key suitable for use with a symmetric cipher.
281
OpenSSL::Cipher::Cipher.new(SYM_ALG).random_key
284
#----------------------------------------------------------------------------#
287
# Return the public key from the X509 certificate file ((|filename|)).
289
def Crypto.certfile2pubkey(filename)
291
File.open(filename) do |f|
292
return cert2pubkey(f)
294
rescue Exception => e
295
raise "error reading certificate file #{filename}: #{e.message}"
299
#----------------------------------------------------------------------------#
301
def Crypto.cert2pubkey(data)
303
return OpenSSL::X509::Certificate.new(data).public_key
304
rescue Exception => e
305
raise "error reading certificate: #{e.message}"
309
#----------------------------------------------------------------------------#
312
# Sign the data from IO stream or string ((|data|)) using the key in
315
# Return the signature.
317
def Crypto.sign(data, keyfilename)
318
raise ArgumentError.new('data') unless data
319
raise ArgumentError.new("invalid file name: #{keyfilename}") unless FileTest.exists?(keyfilename)
321
# Create an IO stream from the data if necessary.
322
io = (data.instance_of?(StringIO) ? data : StringIO.new(data))
324
sha = OpenSSL::Digest::SHA1.new
325
pk = loadprivkey( keyfilename )
326
return pk.sign(sha, io.read )
329
#------------------------------------------------------------------------------#
332
# Generate the SHA1 fingerprint for a PEM-encoded certificate (NOT private key)
333
# Returns the fingerprint in aa:bb:... form
334
# Raises ArgumentError if the fingerprint cannot be obtained
336
def Crypto.cert_sha1_fingerprint(cert_filename)
337
raise ArgumentError.new('cert_filename is nil') if cert_filename.nil?
338
raise ArgumentError.new("invalid cert file name: #{cert_filename}") unless FileTest.exists?(cert_filename)
341
IO.popen("openssl x509 -in #{cert_filename} -noout -sha1 -fingerprint") do |io|
343
md = SHA1_FINGERPRINT_REGEX.match(out)
349
raise ArgumentError.new("could not generate fingerprint for #{cert_filename}") if fingerprint.nil?
354
#------------------------------------------------------------------------------#
356
def Crypto.loadprivkey filename
358
OpenSSL::PKey::RSA.new( File.open( filename,'r' ) )
359
rescue Exception => e
360
raise "error reading private key from file #{filename}: #{e.message}"
364
#----------------------------------------------------------------------------#
367
# XOR the byte string ((|a|)) with the byte string ((|b|)). The operans must
368
# be of the same length.
371
raise ArgumentError.new('data lengths differ') unless a.size == b.size
374
xored << (a[i] ^ b[i])