Stephen Karger Stephen Karger - 7 months ago 46
Ruby Question

DES3 encryption: ruby openssl::cipher vs. oracle dbms_obfuscation_toolkit

I have a legacy application written in PL/SQL that encrypts and decrypts data using 3DES. Now I need to perform similar encryption from a ruby app. Eventually the resulting hash will need to be decrypted by the same PL/SQL application using its existing algorithm.

The problem is that I'm obtaining different encrypted results in PL/SQL and Ruby and I don't know why.

First here is exactly how the PL/SQL encryption works:

From Oracle's docs about DBMS_OBFUSCATION_TOOLKIT http://docs.oracle.com/cd/B19306_01/appdev.102/b14258/d_obtool.htm

"Oracle's implementation of 3DES supports either a 2-key or 3-key implementation, in outer cipher-block-chaining (CBC) mode."

Function signature:

DBMS_OBFUSCATION_TOOLKIT.DES3Encrypt(
input_string IN VARCHAR2,
key_string IN VARCHAR2,
encrypted_string OUT VARCHAR2,
which IN PLS_INTEGER DEFAULT TwoKeyMode
iv_string IN VARCHAR2 DEFAULT NULL);


Note about the parameter which:
"If = 0, (default), then TwoKeyMode is used. If = 1, then ThreeKeyMode is used."
This helped me choose the cipher in the ruby version.

Here is how the application makes that call:

set serveroutput on;
declare
v_encrypted varchar2(100);
begin
dbms_obfuscation_toolkit.des3encrypt(
input_string => 'abcdefgh', -- data to encrypt
key_string => '16_byte_string_k', -- 16 byte = 128 bit key needed by DES3Encrypt
encrypted_string => v_encrypted,
iv_string => 'xxxxxxxx'); -- initialization vector
dbms_output.put_line( lower(utl_raw.cast_to_raw(v_encrypted)) );
-- prints 23ff779e88e2dbe1
end;


Second here is what I'm trying in Ruby:

OpenSSL::Cipher docs:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/Cipher.html

OpenSSL docs to give me the cipher name:
From http://www.openssl.org/docs/apps/enc.html
"des-ede-cbc Two key triple DES EDE in CBC mode"

require 'openssl'

cipher = OpenSSL::Cipher.new('des-ede-cbc')
cipher.encrypt
input = 'abcdefgh'
cipher.key = '16_byte_string_k'
cipher.iv = 'xxxxxxxx'

# i noticed that cipher.update returns same length hash as PL/SQL
# if called without cipher.final, but you are not supposed to do that
#encrypted = cipher.update(input)
encrypted = cipher.update(input) + cipher.final

hex_representation = encrypted.unpack("H*")

puts hex_representation
# prints a5cfc96485d7203eb929c28ceb9fcd53


As shown in the code the ruby version computes a different hash value. Why? What needs to change to make them consistent?

Points I'm unsure about:


  • Whether des-ede-cbc is in fact the same as what Oracle does.

  • Whether utl_raw.cast_to_raw and unpack("H*") will do the same thing
    to the encrypted binary data.

  • What exactly cipher.final appends and if there's any equivalent
    way to append that data in PL/SQL.



Note: I am aware that DES is insecure and that AES has superseded it. My use case does not require these hashes to be unbreakable. The important requirement is to make the hashes consistent so that the PL/SQL app can decrypt hashes generated by the ruby app.

Answer

Let's go digging!

['des-cbc', 'des', 'des-cfb', 'des-ofb', 'des-ecb',
 'des-ede-cbc', 'des-ede', 'des-ede-cfb', 'des-ede-ofb', 
 'des-ede3-cbc', 'des-ede3', 'des3', 'des-ede3-cfb', 
 'des-ede3-ofb', 'desx'].each do |flavour|
  begin
    c = OpenSSL::Cipher.new flavour
    c.encrypt
    c.key = '16_byte_string_k'
    c.iv = 'xxxxxxxx'
    str = 'abcdefgh'
    enc = c.update(str) + c.final
    puts "#{flavour} gives us #{enc.unpack('H*')}"
  rescue => e
    puts "#{flavour} didn't work because #{e.message}"
  end
end

The results:

des-cbc gives us ["a5cfc96485d7203eb929c28ceb9fcd53"]
des gives us ["a5cfc96485d7203eb929c28ceb9fcd53"]
des-cfb gives us ["d898369e91589ae8"]
des-ofb gives us ["d898369e91589ae8"]
des-ecb gives us ["de8579b342a528b6143594946045d91a"]
des-ede-cbc gives us ["23ff779e88e2dbe1c009dc3105d8ff88"]
des-ede gives us ["0e589e3d85ac83efbb271a2e4a77cf4e"]
des-ede-cfb gives us ["1618988004b6a948"]
des-ede-ofb gives us ["1618988004b6a948"]
des-ede3-cbc didn't work because key length too short
des-ede3 didn't work because key length too short
des3 didn't work because key length too short
des-ede3-cfb didn't work because key length too short
des-ede3-ofb didn't work because key length too short
desx didn't work because key length too short

des-ede-cbc gives you a match--at least the first part matches. The question is, why is the encrypted body longer? I'm going to bet this is the correct content and the PL/SQL version is truncated somehow--I'll see if I can figure it out.

Edit: nope, it's the padding. When you set the padding to 0 on the cipher, you get the same results as the PL/SQL version, e.g.

['des-cbc', 'des', 'des-cfb', 'des-ofb', 'des-ecb',
 'des-ede-cbc', 'des-ede', 'des-ede-cfb', 'des-ede-ofb', 
 'des-ede3-cbc', 'des-ede3', 'des3', 'des-ede3-cfb', 
 'des-ede3-ofb', 'desx'].each do |flavour|
  begin
    c = OpenSSL::Cipher.new flavour
    c.encrypt
    c.key = '16_byte_string_k'
    c.iv = 'xxxxxxxx'
    c.padding = 0 # This is the important part!
    str = 'abcdefgh'
    enc = c.update(str) + c.final
    puts "#{flavour} gives us #{enc.unpack('H*')}"
  rescue => e
    puts "#{flavour} didn't work because #{e.message}"
  end
end

...
des-ede-cbc gives us ["23ff779e88e2dbe1"]
...

You will need to compare the two algorithms with different lengths of input string now. Take a look at the documentation for the padding method here: http://www.ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/Cipher.html