diff --git a/lib/aes/aes.rb b/lib/aes/aes.rb index fc35f38..531ed85 100644 --- a/lib/aes/aes.rb +++ b/lib/aes/aes.rb @@ -4,10 +4,22 @@ class << self def encrypt(plain_text, key, opts={}) ::AES::AES.new(key, opts).encrypt(plain_text) end + + # Encrypts an file + def encrypt_file(filepath, enc_filepath, key, opts={}) + ::AES::AES.new(key, opts).encrypt_file(filepath, enc_filepath) + end + # Decrypts the cipher_text with the provided key def decrypt(cipher_text, key, opts={}) ::AES::AES.new(key, opts).decrypt(cipher_text) end + + # Decrypts an file + def decrypt_file(filepath, enc_filepath, key, opts={}) + ::AES::AES.new(key, opts).decrypt_file(filepath, enc_filepath) + end + # Generates a random key of the specified length in bits # Default format is :plain def key(length=256,format=:plain) @@ -19,7 +31,7 @@ def key(length=256,format=:plain) key end end - # Generates a random iv + # Generates a random iv # Default format is :plain def iv(format=:plain) iv = ::AES::AES.new("").random_iv @@ -28,10 +40,10 @@ def iv(format=:plain) Base64.encode64(iv).chomp else iv - end + end end end - + class AES attr :options attr :key @@ -39,7 +51,10 @@ class AES attr :cipher attr :cipher_text attr :plain_text - + + ENCRYPT_CHUNK_SIZE = 2048 + DECRYPT_CHUNK_SIZE = 2822 #it uses base64 on the encrypted chunk. this is the size after the base64 encoding. + def initialize(key, opts={}) merge_options opts @cipher = nil @@ -47,7 +62,7 @@ def initialize(key, opts={}) @iv ||= random_iv self end - + # Encrypts def encrypt(plain_text) @plain_text = plain_text @@ -62,7 +77,18 @@ def encrypt(plain_text) @cipher_text end - # Decrypts + def encrypt_file(filepath, encrypted_filepath) + enc_file = File.open(encrypted_filepath, 'wb') + File.open(filepath, 'rb') do |f| + until f.eof + chunk = f.read(ENCRYPT_CHUNK_SIZE) + enc_file.write(encrypt(chunk)) + end + end + enc_file.close + end + + # Decrypts def decrypt(cipher_text) @cipher_text = cipher_text _setup(:decrypt) @@ -73,7 +99,18 @@ def decrypt(cipher_text) ctext = @cipher_text end @cipher.iv = ctext[0] - @plain_text = @cipher.update(ctext[1]) + @cipher.final + @plain_text = @cipher.update(ctext[1]) + @cipher.final + end + + def decrypt_file(encrypted_filepath, filepath) + file = File.open(filepath, 'wb') + File.open(encrypted_filepath, 'rb') do |f| + until f.eof + chunk = f.read(DECRYPT_CHUNK_SIZE) + file.write(decrypt(chunk)) + end + end + file.close end # Generate a random initialization vector @@ -81,24 +118,24 @@ def random_iv _setup(:encrypt) @cipher.random_iv end - + # Generate a random key def random_key(length=256) _random_seed.unpack('H*')[0][0..((length/8)-1)] end - + private - + # Generates a random seed value def _random_seed(size=32) if defined? OpenSSL::Random return OpenSSL::Random.random_bytes(size) else chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a - (1..size).collect{|a| chars[rand(chars.size)] }.join + (1..size).collect{|a| chars[rand(chars.size)] }.join end end - + # Un-Base64's the IV and CipherText # Returns an array containing the IV, and CipherText def b64_d(data) @@ -108,12 +145,12 @@ def b64_d(data) end iv_and_ctext end - + # Base64 Encodes a string def b64_e(data) Base64.encode64(data).chomp end - + # Encrypts @plain_text def _encrypt @cipher.update(@plain_text) + @cipher.final @@ -128,7 +165,7 @@ def merge_options(opts) }.merge! opts _handle_iv end - + def _handle_iv @iv = @options[:iv] return if @iv.nil? @@ -138,13 +175,13 @@ def _handle_iv @iv = Base64.decode64(@options[:iv]) end end - + # Create a new cipher using the cipher type specified def _setup(action) - @cipher ||= OpenSSL::Cipher::Cipher.new(@options[:cipher]) + @cipher ||= OpenSSL::Cipher::Cipher.new(@options[:cipher]) # Toggles encryption mode @cipher.send(action) @cipher.key = @key.unpack('a2'*32).map{|x| x.hex}.pack('c'*32) end end -end \ No newline at end of file +end diff --git a/test/test_aes.rb b/test/test_aes.rb index 33bc3f5..b0cb6a0 100644 --- a/test/test_aes.rb +++ b/test/test_aes.rb @@ -1,7 +1,7 @@ require 'helper' class TestAES < Test::Unit::TestCase - + should "encrypt and decrypt a string" do key = "01234567890123456789012345678901" msg = "This is a message that nobody should ever see" @@ -10,7 +10,7 @@ class TestAES < Test::Unit::TestCase enc = AES.encrypt(msg, key, {:format => :plain}) assert_equal msg, AES.decrypt(enc, key, {:format => :plain}) end - + should "produce the same encrypted string when provided an identical key and iv" do key = "01234567890123456789012345678901" msg = "This is a message that nobody should ever see" @@ -19,10 +19,32 @@ class TestAES < Test::Unit::TestCase enc2 = AES.encrypt(msg, key, {:iv => iv}) assert_equal enc1, enc2 end - + should "generate a new key when AES#key" do assert_equal 32, AES.key.length assert_equal 44, AES.key(256, :base_64).length end - + + should "encrypt and decrypt a file" do + key = "01234567890123456789012345678901" + msg = "This is a message that nobody should ever see" + File.open('message.txt', 'w') { |f| f.write(msg) } + + AES.encrypt_file('message.txt', 'encrypted.txt', key) + assert_not_equal(File.read('message.txt'), File.read('encrypted.txt')) + + AES.decrypt_file('encrypted.txt', 'decrypted.txt', key) + assert_equal(File.read('message.txt'), File.read('decrypted.txt')) + + end + + def teardown + files = ['message.txt', 'encrypted.txt', 'decrypted.txt'] + files.each do |file| + if File.exists?(file) + File.delete(file) + end + end + end + end