This took some time to figure out, so I thought I'd publish it, maybe spare someone a few grey hairs.
The situation: I have a password stored by Ruby (Ruby on Rails to be specific, but it doesn't matter), with symmetric encryption. I want to decipher it using PHP.
This is the encrypting in Ruby:
require 'openssl'
require 'base64'
cipher = OpenSSL::Cipher::Cipher.new("des-ede3-cbc")
key = "secretkey"
cipher.encrypt(key)
data_to_encode = "karman"
encoded_data = cipher.update(data_to_encode)
encoded_data << cipher.final
puts Base64.encode64(encoded_data)
The problem is that the key is not the key for the encryption. Rather, it's a salt, which is used by OpenSSL class in Ruby to generate a key and an IV (initialization vector) for the cipher.
See the source at: \ruby\src\ruby-1.8.6-p111\ext\openssl\ossl_cipher.c lines 156-197
This is the essence:
EVP_BytesToKey(EVP_CIPHER_CTX_cipher(ctx), EVP_md5(), iv, RSTRING(pass)->ptr, RSTRING(pass)->len, 1, key, NULL);
The variable names are a bit confusing, 'pass' is the supplied salt (in this case "secretkey", 'iv' is a default value "OpenSSL for Ruby rulez!", 'key' is the returned cipher key and the last parameter, which is NULL should be the returned, generated IV. This generated IV is not used by Ruby, the IV is the default value set in 'iv', truncated to the required size.
OpenSSL documentation of the EVP_BytesToKey functionTo be able to decipher the data, we need the same key and IV in PHP. The function below creates the key and IV.
function getkeyiv($key_size, $iv_size, $key, $iv='') {
if ($iv == '') {
$iv = 'OpenSSL for Ruby rulez!';
}
$iv = substr($iv,0,$iv_size);
$gen = '';
do {
$gen = $gen.md5($gen.$key.$iv,true);
} while (strlen($gen)<($iv_size+$key_size));
$o[0] = substr($gen,0,$key_size);
$o[1] = $iv; //this is not the generated IV,
//just the original "salt" cut to the required size
return $o;
}
Possible modifications if needed: the hashing algorithm can be anything, in this case I'm using MD5 because that's set in Ruby. But it can be SHA-1 or anything supported by OpenSSL.
In OpenSSL there is a 'count' parameter which is 1 in Ruby, so I did not bother with using it. It's the number of times, the data is hashed. So if count=1 then the hashing is: md5(data), if count=2 then md5(md5(data)), etc. It can be easily implemented in the function.
The generated IV is not returned by the function (like in Ruby), but if you need it:
$generated_iv = substr($gen,$key_size, $iv_size);
The key size and IV size varies depending on the cipher algorithm, so I pass those as parameters.
The decrypted string is padded with N bytes of characters of N ascii code, i.e.: original string is 6 bytes then padded with 2 bytes of \002 characters to be 8 bytes block. If exactly 8 bytes, 8 bytes of \008 is added! \000-\008 characters are safe to simply trim from a plaintext data.
The block size can alsovary depending on the cipher algorithm, this funciton does not care about that, always assumes that block size is 8 bytes!
function trimpadding($str) {
if (ord($str[strlen($str)-1]) < 9) {
return rtrim($str,$str[strlen($str)-1]);
} else {
return $str;
}
}
As far as I'm aware, the functions above are multibyte safe, but if you use mbstring.func_overload then strlen() and substr() will not work as expected. So don't do that.
And this is the decipher function itself, using mcrypt in Triple DES CBC mode. I generate the key and IV and trim the deciphered data.
function decipher($encoded) {
$key = 'secretkey';
$decrypted='';
$enc = base64_decode($encoded);
$td = mcrypt_module_open(MCRYPT_TRIPLEDES, '', MCRYPT_MODE_CBC, '');
$iv_size = mcrypt_enc_get_iv_size($td);
$key_size = mcrypt_enc_get_key_size($td);
$genkey = getkeyiv($key_size, $iv_size,$key);
if (mcrypt_generic_init($td,$genkey[0],$genkey[1]) != -1) {
$decrypted = mdecrypt_generic($td, $enc);
}
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return trimpadding($decrypted);
}
