Tuesday, August 26, 2008

DES encryption: Java to OpenSSL to Ruby

Encryption has always been an interest of mine, but I really don't have mad skillz. I must admit that I ran into a bit of difficulty in trying to figure out how to properly use the OpenSSL API for Ruby, but what do you expect? The documentation is frustratingly poor.

In parallel with an ongoing project at work, I took some notes on using DES encryption with Java to generate the proper bits for a DES key; OpenSSL to actually use this key to encrypt some file; and then use Ruby/OpenSSL to decrypt. I know, I know: DES has been cracked for some time now. But this an exercise just to see if I could bring together what little acumen I possess in Java, Ruby, and OpenSSL.

Here's how I did it for DES using the Cipher Block Chain operation mode. Note that the DES key and initialization vector are being passed from Java to OpenSSL to Ruby as a string of hexadecimal char, but in reality we are dealing with bytes.

First, we create a valid DES key and set of initialization vector bytes. From the Federal Information Processing Standards Publication 46-2, the specification for the Data Encryption Standard (DES):

A key consists of 64 binary digits ("O"s or "1"s) of which 56 bits are randomly generated and used directly by the algorithm. The other 8 bits, which are not used by the algorithm, are used for error detection. The 8 error detecting bits are set to make the parity of each 8-bit byte of the key odd, i.e., there is an odd number of "1"s in each 8-bit byte.


We'll use Java and the javax.crypto.KeyGenerator factory to create a valid DES key and initialization vector:

// in Java

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

...

public static void main(String[] args) throws Exception {
// create a DES key
KeyGenerator kg = KeyGenerator.getInstance("DES");
SecretKey key = kg.generateKey();
byte[] keyBytes = key.getEncoded();
StringBuffer sbuf = new StringBuffer();
for (byte b : keyBytes) {
sbuf.append(String.format("%02x", (b & 0xFF)));
}
System.out.println("DES key: " + sbuf);

// create initialization parameter bytes
Cipher nonceCipher = Cipher.getInstance("DES/CBC/PKCS5padding");
nonceCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(new byte[8]));
IvParameterSpec iv = new IvParameterSpec(nonceCipher.doFinal(new byte[8]), 0, 8);
byte[] ivBytes = iv.getIV();
sbuf = new StringBuffer();
for (byte b : ivBytes) {
sbuf.append(String.format("%02x", (b & 0xFF)));
}
System.out.println("IV: " + sbuf);
}


The above Java code will output a string of 8 bytes in hexadecimal for the DES key, and also a string of 8 bytes in hexadecimal for the initialization vector to be used in CBC. We next use this as input for the openssl enc program:

... with OpenSSL command-line ...

shino# openssl des-cbc -e -in plaintext.txt -out encrypted.des-cbc \
-K [java-generated DES key string] -iv [java-generated IV string]


The above OpenSSL command will encrypt the file specified with the -in option, using DES-CBC with the given -K DES key and -iv initialization vector. The values for -K and -iv need to be a string of 8 bytes given in hex. Encrypting the plaintext file plaintext.txt will then result in the output encrypted.des-cbc. By the way, this OpenSSL command is using PKCS5 padding as default.

To decrypt with ruby/OpenSSL was a bit tricky. Like I mentioned earlier, the documentation is very, very poor. Assuming that the DES-CBC encrypted file encrypted.des-cbc exists in our current working directory, we can fire up irb to do the decryption and bring it all home:

# with irb/ruby/openssl

# given key = "838519bcc21fb0a1"
# given iv = "33bc52494428f4b6"
# given plaintext msg = "hello, encryption!\n"

irb(main):001:0> require 'openssl'
=> true
irb(main):002:0> f = File.open('encrypted.des-cbc', 'rb')
=> #<file:encrypted.des-cbc>
irb(main):003:0> data = f.readline
=> ...
irb(main):004:0> f.close
=> nil
irb(main):005:0> decrypt = OpenSSL::Cipher::Cipher.new('des-cbc')
=> #<openssl::cipher::cipher:0x4417d60>
irb(main):006:0> decrypt.decrypt
=> #<openssl::cipher::cipher:0x4417d60>
irb(main):007:0> decrypt.key = "838519bcc21fb0a1".scan(/../).map{|b|b.hex}.pack('c*')
=> "\203\205\031\274\302\037\260\241"
irb(main):008:0> decrypt.iv = "33bc52494428f4b6".scan(/../).map{|b|b.hex}.pack('c*')
=> "3\274RID(\364\266"
irb(main):009:0> decrypt.update(data) + decrypt.final
=> "hello, encryption!\n"


Here, we use irb to open the encrypted file and read in the bytes. Then, we use the ruby/OpenSSL API to create a DES-CBC cipher. The call to Cipher.decrypt sets the cipher to work in decryption mode. We next specify the DES key and IV initialization vector, as well as the string of encrypted chars as binary strings.

# ruby snippet
"838519bcc21fb0a1".scan(/../).map{|b|b.hex}.pack('c*')

In setting the key and iv, we provide a 16-char long string of hexadecimal that represents 8-byte values. The above invocation uses String.scan(/../) to break the hex string into a list of 2-char long chunks. Enumerable.map{ |b| b.hex } then maps each 2-char hex chunk back into its numerical (byte) equivalent. Array.pack('c*') lastly transforms the entire list into a single binary sequence. The documentation for Cipher.key= and Cipher.iv= is none too clear that both APIs expect a binary string. I spent quite a bit of time scratching my head as I was mistakenly passing in the plain-vanilla hexadecimal strings for the key and iv values.

Anyhoo, as you have seen, it is not all that hard to do a round-trip DES-CBC encryption using Java to generate the key and iv; OpenSSL to actually encrypt the plaintext; and ruby/OpenSSL to decrypt.

BTW, this doc page in Japanese was a bit more helpful in pointing me in the right direction.

No comments: