Search code examples
rubyencryptionjrubymcryptrijndael

rijndael_128 ruby without mcrypt


Hello I have next implementation of rijdael_128 encyption/decription in ruby

require 'mcrypt'
class Crypt

  attr_accessor :key, :iv

  def initialize(key, iv = nil)
    self.key = key
    self.iv = iv

    @enc = Mcrypt.new(:rijndael_128, :cbc, normalize_key, self.iv, :pkcs)
  end

  def encrypt(data)
    @enc.encrypt(data)
  end

  def decrypt(data)
    @enc.decrypt(data).gsub(/[^0-9#,]/, '') # clean up last \a symbols
  end

  protected
  def normalize_key

    return self.key if [16, 24, 32].include?(self.key.length)

    if self.key.length < 16
      self.key.split.pack('a16')
    elsif self.key.length < 24
      self.key.split.pack('a24')
    elsif self.key.length < 32
      self.key.split.pack('a32')
    elsif self.key.length > 32
      self.key[0..31]
    end
  end

end

Is there a way to implement this without mcrypt ? want to use Cipher, however I have different results when key length > 16

class Crypt2

  attr_accessor :key, :iv

  def initialize(key, iv = nil)
    self.key = key
    self.iv = iv
  end

   def encrypt(data)
        cipher = OpenSSL::Cipher.new('AES-128-CBC')
        cipher.encrypt
        cipher
        cipher.key = normalize_key
        cipher.iv = self.iv
        enc =  cipher.update(data)
        enc  << cipher.final
    end

    protected
    def normalize_key

      return self.key if [16, 24, 32].include?(self.key.length)

      if self.key.length < 16
        self.key.split.pack('a16')
      elsif self.key.length < 24
        self.key.split.pack('a24')
      elsif self.key.length < 32
        self.key.split.pack('a32')
      elsif self.key.length > 32
        self.key[0..31]
      end
    end

end

Same results with key = "1234567890"

1.9.3-p547 :498 > key = "1234567890"
     => "1234567890" 
    1.9.3-p547 :499 > iv 
     => "0000001409228008" 
    1.9.3-p547 :500 > data
     => "1409227523#143620#16502300493" 
    1.9.3-p547 :501 > Crypt.new(key,iv).encrypt(data)
     => "\xFB\x16\a\xFF\x9ED\xA8\xD7\x1F=k\x8E\xFFH\xB0\x17\x84:\x1Fa\xB8s\x14\x97%S\xF3\x1E_\xDF\xBB\x19" 
    1.9.3-p547 :502 > Crypt2.new(key,iv).encrypt(data)
     => "\xFB\x16\a\xFF\x9ED\xA8\xD7\x1F=k\x8E\xFFH\xB0\x17\x84:\x1Fa\xB8s\x14\x97%S\xF3\x1E_\xDF\xBB\x19" 

Different results with larger key

1.9.3-p547 :503 > key = key * 2
         => "12345678901234567890" 
        1.9.3-p547 :504 > Crypt.new(key,iv).encrypt(data)
         => "\x1A\xE61\xD7\xC8;\xE0M\xFA\xD4~[\xBA7N\xD9\xB9\xE2\x94\x8C\xA89\x99\xD9}\x82,9\xFE\xF5\xFA\x00" 
        1.9.3-p547 :505 > Crypt2.new(key,iv).encrypt(data)
         => "10X.\"\xF3\xC3RO`\t\x17\xB43\"r\x87s\xCF\xEA\x93Y4z\xCC\xC9\xAFA\xA1\x80\xC9\xF7" 

Solution

  • As I commented before, you are explicitly initializing Cipher/mcrypt with AES-128/rijndael-128. That means the encryption function expects a key with length 128 bit/16 bytes.

    It's behavior is undefined when you pass a larger key. (It could throw an error, could shrink the key, could do encryption with the larger key, or could do something else.)

    It seems that Cipher and mcrypt handle this case differently and hence give different outputs.


    Tests:

    I could not find any statement on that case in either of the docs and so did some research on my own. For reference, this is the test-code I used:

    require "rubygems"
    require "openssl"
    require "mcrypt"
    
    def encryptOpenSSL(iv, key, data, key_length)
        cipher = OpenSSL::Cipher.new("AES-" + key_length.to_s + "-CBC")
        cipher.encrypt
        cipher.key = key
        cipher.iv = iv
    
        return (cipher.update(data) + cipher.final).unpack("H*").join()
    end
    
    def encryptMcrypt(iv, key, data)
        cipher = Mcrypt.new(:rijndael_128, :cbc, key, iv, :pkcs)
    
        return cipher.encrypt(data).unpack("H*").join()
    end
    
    # test parameters
    data =   "This is my test-data!"
    key128 = "1234567890123456"
    key256 = "1234567890123456abcdefghijklmnop"
    iv  =    "0987654321098765"
    
    # tests
    puts "OpenSSL AES(128) key=128bit: " + encryptOpenSSL(iv, key128, data, 128)
    puts "OpenSSL AES(128) key=256bit: " + encryptOpenSSL(iv, key256, data, 128)
    puts "Mcrypt  AES(128) key=128bit: " +  encryptMcrypt(iv, key128, data)
    puts "Mcrypt  AES(128) key=256bit: " +  encryptMcrypt(iv, key256, data)
    puts "OpenSSL AES(256) key=256bit: " + encryptOpenSSL(iv, key256, data, 256)
    

    Which outputs for me (on ruby 1.9.1):

    "OpenSSL AES(128) key=128bit: adffaed8c94ede8aa61138b3fe500e30a0 ..."
    "OpenSSL AES(128) key=256bit: adffaed8c94ede8aa61138b3fe500e30a0 ..."
    "Mcrypt  AES(128) key=128bit: adffaed8c94ede8aa61138b3fe500e30a0 ..."
    "Mcrypt  AES(128) key=256bit: b07776231d1bfbd2dfe3f8a62affdc4223 ..."
    "OpenSSL AES(256) key=256bit: b07776231d1bfbd2dfe3f8a62affdc4223 ..."
    

    Conclusion:

    Looking at the test results, you can easily see that Cipher still uses only the first 128 bits when you pass a larger key than expected.

    Whereas mcrypt() performs rijndael-256 encryption when you pass a 256 bit key (even though you have set rijndael-128 before).


    Solution:

    Assuming you want Cipher to encrypt with AES-256 when you pass a 256 bit key, you could dynamically set the key_size depending on your input key.length. Like this:

    # `* 8` to convert from `bytes` to `bits`
    cipher = OpenSSL::Cipher.new('AES-' + (normalize_key.length * 8).to_s + '-CBC')