Encrypting/Decrypting 3Des in Ruby

Encrypting/decrypting 3DES in Ruby

Got it working!

Here's how:

Decryption

def read_key(key_file)
File.read(key_file).split(',').map { |x| x.to_i }.pack('c*')
end
des = OpenSSL::Cipher::Cipher.new('des-ede3')
des.decrypt
des.key = read_key('key.bin')

result = des.update(decoded) + des.final

Encryption

def read_key(key_file)
File.read(key_file).split(',').map { |x| x.to_i }.pack('c*')
end
des2 = OpenSSL::Cipher::Cipher.new('des-ede3')
des2.encrypt
des2.key = read_key('key.bin')

result = des2.update(result) + des2.final
puts Base64.encode64(result)

Decrypt hex strings using 3DES in Ruby from .NET

A 3DES key is 24-bytes, use a full length key.

3DES uses triple encryption with essentially a 24-byte key. 202FA9B21843D7022B6466DB68327E1F is hex encoded 16-byte key.

Try repeating the first 8-bytes of the key:

202FA9B21843D7022B6466DB68327E1F202FA9B21843D702

Some 3DES implementations will repeat 8-bytes of a 16-byte key but relying on such implementation details is not a good idea.

Note: 3DES actually uses a 168-bit key because the LSb of each byte is not used. Further because there are actually three DES calls the security is only 112-bits. Additionally DES has some weak keys. There are two common modes, ede and ded, in an effort to facilitate moving from DES to 3DES thus adding more confusion.

Finally: Move from 3DES to AES in CBC mode with a random IV. Please don't continue poor security practices.

DES3 decryption in Ruby on Rails

The documentation for OpenSSL::Cipher states:

Make sure to call .encrypt or .decrypt before using any of the following
methods:

  • [key=, iv=, random_key, random_iv, pkcs5_keyivgen]

In your specific case, omitting the call to cipher.decrypt causes a bad decrypt error, as you've seen.

The following example corrects that problem and exhibits the expected behavior:

require 'openssl'
require 'Base64'

# For testing purposes only!
message = 'MyTestString'
key = 'PasswordPasswordPassword'
iv = '12345678'

# Encrypt plaintext using Triple DES
cipher = OpenSSL::Cipher::Cipher.new("des3")
cipher.encrypt # Call this before setting key or iv
cipher.key = key
cipher.iv = iv
ciphertext = cipher.update(message)
ciphertext << cipher.final

puts "Encrypted \"#{message}\" with \"#{key}\" to:\n\"#{ciphertext}\"\n"

# Base64-encode the ciphertext
encodedCipherText = Base64.encode64(ciphertext)

# Base64-decode the ciphertext and decrypt it
cipher.decrypt
plaintext = cipher.update(Base64.decode64(encodedCipherText))
plaintext << cipher.final

# Print decrypted plaintext; should match original message
puts "Decrypted \"#{ciphertext}\" with \"#{key}\" to:\n\"#{plaintext}\"\n\n"

Ruby OpenSSL::Cipher giving different result Tripple DES in CBC Mode

To get your expected results, you need to adjust your code so that OpenSSL actually knows that you are using hex-encoded data. Since OpenSSL deals with plain bytes anyway, you thus need to encode your hex string to raw bytes first:

key = ["2315208C9110AD402315208C9110AD40"].pack('H*')
iv = ["0000000000000000"].pack('H*')
input_text = ["2020205a4f534135366461746574696d653d32303138313032343130303332333b6578706972793d313232323b70616e3d343233363031373839303132333435362121212121212121"].pack('H*')

After that, you need to chose the right encryption algorithm. There are multiple distinct flavours of 3DES which (among other details) differ in the expected key length. Since you are dealing with a 16 byte key, it appears you are using what OpenSSL calls des-ede-cbc

flavour = 'des-ede-cbc'

These adjustments alone should normally be enough to cause the same result as you would see on the website you linked to. However, for some reason unknown to me, they silently change the input data before actually encrypting it.

The specific rules they use are unknown to me but to get the same ciphertext output as the website, you need to remove any trailing exclamation marks and remove a leading space character from your input string.

input_text # packed as you provided it originally
# => " ZOSA56datetime=20181024100323;expiry=1222;pan=4236017890123456!!!!!!!!"

# remove trailing exclamation marks
input_text = input_text.sub(/!*$/, '')
# remove the first character
input_text = input_text[1..-1]

Finally, you can now encrypt your "improved" input_text:

begin
c = OpenSSL::Cipher.new(flavour)
c.encrypt
c.key = key
c.iv = iv
enc = c.update(input_text) + c.final
puts "#{flavour} gives us #{enc.unpack('H*').first.upcase}"
rescue => e
puts "#{flavour} didn't work because #{e.message}"
end

This should result in almost the expected cipher text.

It only slightly differs at the final block of 8 bytes though. I assume the website uses some strange non-standard padding (which is required to ensure the input text can be divided into full 8 byte chunks) which results in the final block being different.

In any case, you can decrypt the result (as well as the "expected" ciphertext form the website with this code:

flavour = 'des-ede-cbc'
key = ["2315208C9110AD402315208C9110AD40"].pack('H*')
iv = ["0000000000000000"].pack('H*')
encrypted = ["4A9E9B245BBDC16D76998143CB6FC1C2B8780539C1C9A100AEC3D745B8BF00DF43A4B51A29A6205845E510E18E26AB940152F90F12E86543A9E5239B30DFDBCD8D3FCDB65F603979"].pack('H*')

c = OpenSSL::Cipher.new(flavour)
c.decrypt
c.key = key
c.iv = iv

decrypted = c.update(encrypted)

How to get the padding right to match the result from the website will be left as an exercise to the reader :) Unfortunately, I have not found any documentation or source code for their tool.

In any case, please note that 3DES is an incredibly outdated encryption algorithm which is not considered secure anymore. Unless you have to actually use 3DES, you should use a more secure algorithm.

The libsodium project provides hardened implementations of secure algorithms for encryption and signing which can form a valid and secure basis for your desired protocol. There are Ruby bindings available with the rbnacl gem.

Ruby 3DES implementation vs PHP mcrypt, different results

I answer my own question.

It was an issue about how openssl and mcrypt implementations use padding. My cryptography knowledge isn't too deep, but I found a usable code sample here http://opensourcetester.co.uk/2012/11/29/zeros-padding-3des-ruby-openssl/

#ENCRYPTION
block_length = 8
des.padding = 0 #Tell Openssl not to pad
des.encrypt
json = '{"somekey":"somevalue"}'
json += "\0" until json.bytesize % block_length == 0 #Pad with zeros
edata = des.update(json) + des.final
b64data = Base64.encode64(edata).gsub("\n",'')

Basically, ruby openssl will use PKCS padding, while mcrypt uses 0 padding. So in our code I had to tell openssl not to pad the string with des.padding = 0 and then do the padding manually: json += "\0" until json.bytesize % block_length == 0.

Those are the important bits that were missing in my original implementation.

How to perform Triple DES calculations in Ruby in hexadecimal?

The key is in hex - if you look at the Java page you pasted you can see that easily by decoding the binary values for the key in the detailed output.

>> des_cbc=OpenSSL::Cipher::Cipher.new("des-ede-cbc")
=> #<OpenSSL::Cipher::Cipher:0x10116ce28>
>> des_cbc.encrypt
=> #<OpenSSL::Cipher::Cipher:0x10116ce28>
>> des_cbc.key="\x23"*8 << "\x45"*8
=> "########EEEEEEEE"
>> des_cbc.update("\x00"*8).unpack('H*')
=> ["3a42d7a1d1c60c40"]


Related Topics



Leave a reply



Submit